Extract provision_merchant() service for shared use
Both _auto_create_merchant (lazy GET fallback in views_api) and LNbits' _create_default_merchant (eager signup hook) used to reimplement merchant + zone + stall creation independently. Moves the canonical implementation to services.provision_merchant() so both call sites stay in lockstep — future changes (NIP-17 kind 10050 relay list, additional default zones, etc.) only happen in one place. - services.provision_merchant(user_id, wallet_id, public_key, private_key, display_name, config): creates merchant if absent, default 'Online' zone, default '<username>'s Store' stall, and publishes the kind 30017 stall event. Idempotent on the merchant pubkey: returns the existing merchant unchanged if one exists. - views_api._auto_create_merchant: now a 10-line wrapper that loads the account, generates fallback keys if missing, then delegates. The LNbits-side hook (lnbits/core/services/users.py:_create_default_merchant) will be updated in a companion commit to also call this service. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e481c9179d
commit
05ebf042ac
2 changed files with 86 additions and 52 deletions
71
services.py
71
services.py
|
|
@ -12,9 +12,11 @@ from .crud import (
|
||||||
CustomerProfile,
|
CustomerProfile,
|
||||||
create_customer,
|
create_customer,
|
||||||
create_direct_message,
|
create_direct_message,
|
||||||
|
create_merchant,
|
||||||
create_order,
|
create_order,
|
||||||
create_product,
|
create_product,
|
||||||
create_stall,
|
create_stall,
|
||||||
|
create_zone,
|
||||||
get_customer,
|
get_customer,
|
||||||
get_last_direct_messages_created_at,
|
get_last_direct_messages_created_at,
|
||||||
get_last_product_update_time,
|
get_last_product_update_time,
|
||||||
|
|
@ -42,6 +44,7 @@ from .models import (
|
||||||
DirectMessage,
|
DirectMessage,
|
||||||
DirectMessageType,
|
DirectMessageType,
|
||||||
Merchant,
|
Merchant,
|
||||||
|
MerchantConfig,
|
||||||
Nostrable,
|
Nostrable,
|
||||||
Order,
|
Order,
|
||||||
OrderContact,
|
OrderContact,
|
||||||
|
|
@ -49,11 +52,13 @@ from .models import (
|
||||||
OrderItem,
|
OrderItem,
|
||||||
OrderStatusUpdate,
|
OrderStatusUpdate,
|
||||||
PartialDirectMessage,
|
PartialDirectMessage,
|
||||||
|
PartialMerchant,
|
||||||
PartialOrder,
|
PartialOrder,
|
||||||
PaymentOption,
|
PaymentOption,
|
||||||
PaymentRequest,
|
PaymentRequest,
|
||||||
Product,
|
Product,
|
||||||
Stall,
|
Stall,
|
||||||
|
Zone,
|
||||||
)
|
)
|
||||||
from .nostr.event import NostrEvent
|
from .nostr.event import NostrEvent
|
||||||
from .nostr.nip59 import unwrap_message, wrap_message
|
from .nostr.nip59 import unwrap_message, wrap_message
|
||||||
|
|
@ -180,6 +185,72 @@ async def sign_and_send_to_nostr(
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
async def provision_merchant(
|
||||||
|
user_id: str,
|
||||||
|
wallet_id: str,
|
||||||
|
public_key: str,
|
||||||
|
private_key: str,
|
||||||
|
display_name: Optional[str] = None,
|
||||||
|
config: Optional[MerchantConfig] = None,
|
||||||
|
) -> Merchant:
|
||||||
|
"""
|
||||||
|
Provision a merchant with a default shipping zone and default stall,
|
||||||
|
and publish the stall to Nostr relays.
|
||||||
|
|
||||||
|
Single source of truth used by:
|
||||||
|
- LNbits user-creation hook (eager, on signup) — see
|
||||||
|
lnbits/core/services/users.py:_create_default_merchant
|
||||||
|
- nostrmarket views_api._auto_create_merchant (lazy, on first GET
|
||||||
|
/api/v1/merchant when a merchant is missing).
|
||||||
|
|
||||||
|
Idempotent on the merchant: if a merchant with this pubkey already
|
||||||
|
exists, returns it without recreating zone/stall.
|
||||||
|
"""
|
||||||
|
existing = await get_merchant_by_pubkey(public_key)
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
|
||||||
|
partial_merchant = PartialMerchant(
|
||||||
|
private_key=private_key,
|
||||||
|
public_key=public_key,
|
||||||
|
config=config or MerchantConfig(),
|
||||||
|
)
|
||||||
|
merchant = await create_merchant(user_id, partial_merchant)
|
||||||
|
|
||||||
|
online_zone = Zone(
|
||||||
|
id=f"online-{merchant.public_key}",
|
||||||
|
name="Online",
|
||||||
|
currency="sat",
|
||||||
|
cost=0,
|
||||||
|
countries=["Free (digital)"],
|
||||||
|
)
|
||||||
|
await create_zone(merchant.id, online_zone)
|
||||||
|
|
||||||
|
name = display_name or "My"
|
||||||
|
default_stall = Stall(
|
||||||
|
wallet=wallet_id,
|
||||||
|
name=f"{name}'s Store",
|
||||||
|
currency="sat",
|
||||||
|
shipping_zones=[online_zone],
|
||||||
|
)
|
||||||
|
default_stall = await create_stall(merchant.id, default_stall)
|
||||||
|
|
||||||
|
# Publish the kind 30017 stall event so customers' clients can resolve
|
||||||
|
# the stall name when they fetch products. Non-fatal on failure: a
|
||||||
|
# later product publish (or webapp self-heal) will retry.
|
||||||
|
try:
|
||||||
|
stall_event = await sign_and_send_to_nostr(merchant, default_stall)
|
||||||
|
default_stall.event_id = stall_event.id
|
||||||
|
await update_stall(merchant.id, default_stall)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(
|
||||||
|
f"[NOSTRMARKET] Failed to publish default stall for "
|
||||||
|
f"merchant {merchant.id}: {ex}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return merchant
|
||||||
|
|
||||||
|
|
||||||
async def handle_order_paid(order_id: str, merchant_pubkey: str):
|
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)
|
||||||
|
|
|
||||||
67
views_api.py
67
views_api.py
|
|
@ -83,6 +83,7 @@ from .models import (
|
||||||
from .services import (
|
from .services import (
|
||||||
build_order_with_payment,
|
build_order_with_payment,
|
||||||
create_or_update_order_from_dm,
|
create_or_update_order_from_dm,
|
||||||
|
provision_merchant,
|
||||||
reply_to_structured_dm,
|
reply_to_structured_dm,
|
||||||
resubscribe_to_all_merchants,
|
resubscribe_to_all_merchants,
|
||||||
send_dm,
|
send_dm,
|
||||||
|
|
@ -99,72 +100,34 @@ async def _auto_create_merchant(
|
||||||
config: MerchantConfig | None = None,
|
config: MerchantConfig | None = None,
|
||||||
) -> Merchant:
|
) -> Merchant:
|
||||||
"""
|
"""
|
||||||
Provision a merchant record from the user's account keypair.
|
Lazy fallback: provision a merchant from the user's account keypair when
|
||||||
Called automatically on first GET or explicitly via POST.
|
the LNbits-side eager provisioning didn't run (e.g., older accounts, or
|
||||||
|
upstream LNbits without our signup hook).
|
||||||
|
|
||||||
Also creates a default "Online" shipping zone and a default stall named
|
Delegates to services.provision_merchant — the canonical implementation.
|
||||||
after the user, then publishes the stall to relays so that any product
|
|
||||||
the user creates references a stall the customer-facing client can find.
|
|
||||||
"""
|
"""
|
||||||
account = await get_account(wallet.wallet.user)
|
account = await get_account(wallet.wallet.user)
|
||||||
assert account, "User account not found"
|
assert account, "User account not found"
|
||||||
|
|
||||||
# In our fork, accounts always have keypairs.
|
# In our fork, accounts always have keypairs. Generate as fallback only
|
||||||
# Generate as fallback only if somehow missing.
|
# if somehow missing (e.g., upstream LNbits where this isn't auto-set).
|
||||||
if not account.pubkey or not account.prvkey:
|
if not account.pubkey or not account.prvkey:
|
||||||
private_key, public_key = generate_keypair()
|
private_key, public_key = generate_keypair()
|
||||||
account.pubkey = public_key
|
account.pubkey = public_key
|
||||||
account.prvkey = private_key
|
account.prvkey = private_key
|
||||||
await update_account(account)
|
await update_account(account)
|
||||||
else:
|
|
||||||
public_key = account.pubkey
|
|
||||||
private_key = account.prvkey
|
|
||||||
|
|
||||||
existing_merchant = await get_merchant_by_pubkey(public_key)
|
merchant = await provision_merchant(
|
||||||
assert existing_merchant is None, "A merchant already uses this public key"
|
user_id=wallet.wallet.user,
|
||||||
|
wallet_id=wallet.wallet.id,
|
||||||
partial_merchant = PartialMerchant(
|
public_key=account.pubkey,
|
||||||
private_key=private_key,
|
private_key=account.prvkey,
|
||||||
public_key=public_key,
|
display_name=account.username,
|
||||||
config=config or MerchantConfig(),
|
config=config,
|
||||||
)
|
|
||||||
|
|
||||||
merchant = await create_merchant(wallet.wallet.user, partial_merchant)
|
|
||||||
|
|
||||||
online_zone = Zone(
|
|
||||||
id=f"online-{merchant.public_key}",
|
|
||||||
name="Online",
|
|
||||||
currency="sat",
|
|
||||||
cost=0,
|
|
||||||
countries=["Free (digital)"],
|
|
||||||
)
|
|
||||||
await create_zone(merchant.id, online_zone)
|
|
||||||
|
|
||||||
# Create + publish a default stall so products created through the UI
|
|
||||||
# always have a published parent. Without this, a product publish lands
|
|
||||||
# on relays referencing a stall_id that no relay has seen, and the
|
|
||||||
# customer client renders "Unknown Stall".
|
|
||||||
display_name = account.username or "My"
|
|
||||||
default_stall = Stall(
|
|
||||||
wallet=wallet.wallet.id,
|
|
||||||
name=f"{display_name}'s Store",
|
|
||||||
currency="sat",
|
|
||||||
shipping_zones=[online_zone],
|
|
||||||
)
|
|
||||||
default_stall = await create_stall(merchant.id, default_stall)
|
|
||||||
try:
|
|
||||||
stall_event = await sign_and_send_to_nostr(merchant, default_stall)
|
|
||||||
default_stall.event_id = stall_event.id
|
|
||||||
await update_stall(merchant.id, default_stall)
|
|
||||||
except Exception as ex:
|
|
||||||
# Non-fatal: merchant is usable; a product publish (or self-heal)
|
|
||||||
# will republish the stall later.
|
|
||||||
logger.warning(
|
|
||||||
f"[NOSTRMARKET] Failed to publish default stall for {merchant.id}: {ex}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await resubscribe_to_all_merchants()
|
await resubscribe_to_all_merchants()
|
||||||
await nostr_client.merchant_temp_subscription(public_key)
|
await nostr_client.merchant_temp_subscription(account.pubkey)
|
||||||
|
|
||||||
return merchant
|
return merchant
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue