Extract provision_merchant() service for shared use
Some checks failed
ci.yml / Extract provision_merchant() service for shared use (pull_request) Failing after 0s
Some checks failed
ci.yml / Extract provision_merchant() service for shared use (pull_request) Failing after 0s
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
83e9660ae5
commit
16e50d67f9
2 changed files with 86 additions and 52 deletions
71
services.py
71
services.py
|
|
@ -12,9 +12,11 @@ from .crud import (
|
|||
CustomerProfile,
|
||||
create_customer,
|
||||
create_direct_message,
|
||||
create_merchant,
|
||||
create_order,
|
||||
create_product,
|
||||
create_stall,
|
||||
create_zone,
|
||||
get_customer,
|
||||
get_last_direct_messages_created_at,
|
||||
get_last_product_update_time,
|
||||
|
|
@ -42,6 +44,7 @@ from .models import (
|
|||
DirectMessage,
|
||||
DirectMessageType,
|
||||
Merchant,
|
||||
MerchantConfig,
|
||||
Nostrable,
|
||||
Order,
|
||||
OrderContact,
|
||||
|
|
@ -49,11 +52,13 @@ from .models import (
|
|||
OrderItem,
|
||||
OrderStatusUpdate,
|
||||
PartialDirectMessage,
|
||||
PartialMerchant,
|
||||
PartialOrder,
|
||||
PaymentOption,
|
||||
PaymentRequest,
|
||||
Product,
|
||||
Stall,
|
||||
Zone,
|
||||
)
|
||||
from .nostr.event import NostrEvent
|
||||
from .nostr.nip59 import unwrap_message, wrap_message
|
||||
|
|
@ -180,6 +185,72 @@ async def sign_and_send_to_nostr(
|
|||
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):
|
||||
try:
|
||||
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 (
|
||||
build_order_with_payment,
|
||||
create_or_update_order_from_dm,
|
||||
provision_merchant,
|
||||
reply_to_structured_dm,
|
||||
resubscribe_to_all_merchants,
|
||||
send_dm,
|
||||
|
|
@ -99,72 +100,34 @@ async def _auto_create_merchant(
|
|||
config: MerchantConfig | None = None,
|
||||
) -> Merchant:
|
||||
"""
|
||||
Provision a merchant record from the user's account keypair.
|
||||
Called automatically on first GET or explicitly via POST.
|
||||
Lazy fallback: provision a merchant from the user's account keypair when
|
||||
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
|
||||
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.
|
||||
Delegates to services.provision_merchant — the canonical implementation.
|
||||
"""
|
||||
account = await get_account(wallet.wallet.user)
|
||||
assert account, "User account not found"
|
||||
|
||||
# In our fork, accounts always have keypairs.
|
||||
# Generate as fallback only if somehow missing.
|
||||
# In our fork, accounts always have keypairs. Generate as fallback only
|
||||
# if somehow missing (e.g., upstream LNbits where this isn't auto-set).
|
||||
if not account.pubkey or not account.prvkey:
|
||||
private_key, public_key = generate_keypair()
|
||||
account.pubkey = public_key
|
||||
account.prvkey = private_key
|
||||
await update_account(account)
|
||||
else:
|
||||
public_key = account.pubkey
|
||||
private_key = account.prvkey
|
||||
|
||||
existing_merchant = await get_merchant_by_pubkey(public_key)
|
||||
assert existing_merchant is None, "A merchant already uses this public key"
|
||||
|
||||
partial_merchant = PartialMerchant(
|
||||
private_key=private_key,
|
||||
public_key=public_key,
|
||||
config=config or MerchantConfig(),
|
||||
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,
|
||||
)
|
||||
|
||||
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 nostr_client.merchant_temp_subscription(public_key)
|
||||
await nostr_client.merchant_temp_subscription(account.pubkey)
|
||||
|
||||
return merchant
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue