fix(profile): accept 'wownero' for default_qr_logo (was xmrcheckout literal)

ProfileResponse schema's default_qr_logo literal was inherited from
xmrcheckout: Literal["monero", "none", "custom"]. The DB column
(models.py) defaults to "wownero" and main.py's migration backfills
NULL → ... but the backfill also wrote "monero", contradicting the
column default.

End result: new users got default_qr_logo="wownero" (from the
column default), but GET /api/core/profile blew up with a Pydantic
ResponseValidationError on serialization because "wownero" isn't in
the literal. UI showed "Unavailable" for both Store ID and Primary
Address because the entire profile response 500'd, leaving
profileData null on the dashboard. API key + webhook secret rendered
fine because those come from cookies, not profileData.

Fix:
- schemas.py: QrLogoMode now Literal["wownero", "monero", "none",
  "custom"] (keep "monero" for backward compat with any rows from
  before the migration ran)
- schemas.py: ProfileResponse.default_qr_logo default → "wownero"
- main.py: backfill UPDATE → 'wownero' (was 'monero'); migration
  column DEFAULT already correct as 'wownero'
- ui/dashboard/page.tsx: TS type for default_qr_logo widened to
  include "wownero"; fallback default → "wownero"
- ui/dashboard/actions.ts: DefaultQrLogoState.logo type widened;
  validation in updateDefaultQrLogo + initialMetadataPayload now
  accepts "wownero"

Suchshop didn't hit this because their wowcheckout user row was
created back when this UI path wasn't being used (they were
provisioned via direct SQL/API). The new shop.wowne.ro merchant
created via the UI today triggered the column default → exposed
the schema mismatch.
This commit is contained in:
jwinterm
2026-05-18 12:35:55 -04:00
parent 3a8d87e9c9
commit 7cebe309aa
4 changed files with 17 additions and 9 deletions
+2 -2
View File
@@ -196,7 +196,7 @@ def startup():
connection.execute(
text(
"ALTER TABLE users "
"ADD COLUMN IF NOT EXISTS default_qr_logo VARCHAR DEFAULT 'monero'"
"ADD COLUMN IF NOT EXISTS default_qr_logo VARCHAR DEFAULT 'wownero'"
)
)
connection.execute(
@@ -248,7 +248,7 @@ def startup():
)
connection.execute(
text(
"UPDATE users SET default_qr_logo = 'monero' "
"UPDATE users SET default_qr_logo = 'wownero' "
"WHERE default_qr_logo IS NULL"
)
)
+6 -2
View File
@@ -18,7 +18,11 @@ from .formatting import format_xmr_amount
InvoiceStatus = Literal["pending", "payment_detected", "confirmed", "expired", "invalid"]
BtcpayCheckoutStyle = Literal["standard", "btcpay_classic"]
QrLogoMode = Literal["monero", "none", "custom"]
# "wownero" is the wowcheckout default; "monero" kept as an accepted
# value for backward compat with any rows written before the
# fork-rename. New rows are created with "wownero" (see models.py
# server_default + main.py migration backfill).
QrLogoMode = Literal["wownero", "monero", "none", "custom"]
class InvoiceCreate(BaseModel):
@@ -200,7 +204,7 @@ class ProfileResponse(BaseModel):
id: UUID
payment_address: str
default_confirmation_target: conint(ge=0, le=10) = 1
default_qr_logo: QrLogoMode = "monero"
default_qr_logo: QrLogoMode = "wownero"
default_qr_logo_data_url: str | None = None
btcpay_checkout_style: BtcpayCheckoutStyle = "btcpay_classic"
created_at: datetime
+7 -3
View File
@@ -51,7 +51,7 @@ export type DefaultConfirmationTargetState = {
};
export type DefaultQrLogoState = {
logo: "monero" | "none" | "custom";
logo: "wownero" | "monero" | "none" | "custom";
logoDataUrl: string | null;
error: string | null;
success: string | null;
@@ -274,6 +274,7 @@ export async function createInvoiceAction(
const qrLogoDataUrl = String(formData.get("qr_logo_data_url") ?? "").trim();
const qrLogoMode =
qrLogoModeRaw === "account_default" ||
qrLogoModeRaw === "wownero" ||
qrLogoModeRaw === "monero" ||
qrLogoModeRaw === "none" ||
qrLogoModeRaw === "custom"
@@ -540,7 +541,9 @@ export async function updateDefaultQrLogoAction(
const logo = String(formData.get("default_qr_logo") ?? "").trim();
const logoDataUrlRaw = String(formData.get("default_qr_logo_data_url") ?? "").trim();
const normalizedLogo =
logo === "none" || logo === "custom" || logo === "monero" ? logo : null;
logo === "none" || logo === "custom" || logo === "wownero" || logo === "monero"
? logo
: null;
if (!normalizedLogo) {
return {
logo: _prevState.logo,
@@ -602,6 +605,7 @@ export async function updateDefaultQrLogoAction(
const resolvedLogo =
data.default_qr_logo === "none" ||
data.default_qr_logo === "custom" ||
data.default_qr_logo === "wownero" ||
data.default_qr_logo === "monero"
? data.default_qr_logo
: normalizedLogo;
@@ -609,7 +613,7 @@ export async function updateDefaultQrLogoAction(
resolvedLogo === "custom" ? data.default_qr_logo_data_url ?? logoDataUrl : null;
revalidatePath("/dashboard");
return {
logo: resolvedLogo as "monero" | "none" | "custom",
logo: resolvedLogo as "wownero" | "monero" | "none" | "custom",
logoDataUrl: resolvedDataUrl ?? null,
error: null,
success: "QR default saved.",
+2 -2
View File
@@ -70,7 +70,7 @@ type ProfileSummary = {
id: string;
payment_address: string;
default_confirmation_target: number;
default_qr_logo: "monero" | "none" | "custom";
default_qr_logo: "wownero" | "monero" | "none" | "custom";
default_qr_logo_data_url: string | null;
btcpay_checkout_style?: "standard" | "btcpay_classic";
created_at: string;
@@ -422,7 +422,7 @@ export default async function DashboardPage({
</div>
<div className="rounded-2xl border border-stroke bg-white/70 p-6 shadow-soft">
<DefaultQrLogoSection
initialLogo={profileData?.default_qr_logo ?? "monero"}
initialLogo={profileData?.default_qr_logo ?? "wownero"}
initialLogoDataUrl={profileData?.default_qr_logo_data_url ?? null}
/>
</div>