Auto-create + publish default stall, republish stall on product publish

Two complementary fixes for the "Unknown Stall" bug, where a customer
sees a product on the relay but the parent stall is missing.

1. _auto_create_merchant() now creates a default "<username>'s Store"
   stall and publishes its kind 30017 event before returning. New users
   land with a fully-published merchant identity, so the very first
   product they create has a known parent stall on relays.

2. POST /api/v1/product (api_create_product) now republishes the parent
   stall before publishing the product. NIP-33 parameterized replaceable
   events make this idempotent, but it self-heals every existing case
   where the stall publish failed or never happened (transient relay
   issues, accounts that pre-date the auto-publish flow, manual stall
   creation that didn't reach all relays).

This complements the LNbits-side fix in core/services/users.py
(_create_default_merchant publishes the stall on signup) and the
webapp self-heal in useMarketStallSelfHeal.ts. With all three layers,
"Unknown Stall" should disappear from the customer view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-03 16:11:15 +02:00
commit e481c9179d

View file

@ -101,6 +101,10 @@ async def _auto_create_merchant(
"""
Provision a merchant record from the user's account keypair.
Called automatically on first GET or explicitly via POST.
Also creates a default "Online" shipping zone and a default stall named
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)
assert account, "User account not found"
@ -127,16 +131,37 @@ async def _auto_create_merchant(
merchant = await create_merchant(wallet.wallet.user, partial_merchant)
await create_zone(
merchant.id,
Zone(
id=f"online-{merchant.public_key}",
name="Online",
currency="sat",
cost=0,
countries=["Free (digital)"],
),
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 nostr_client.merchant_temp_subscription(public_key)
@ -767,6 +792,21 @@ async def api_create_product(
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)