From 2c42c1a3d3dadf7d88399949c0fe79eb6ef14188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bogda=C5=82?= Date: Fri, 13 Mar 2026 08:26:01 +0100 Subject: [PATCH 01/23] feat: Add Vendor Stripe Connect Onboarding --- .env.example | 4 + .../_actions/stripe-connect.test.ts | 131 ++++++++++ .../_actions/stripe-connect.ts | 226 ++++++++++++++++++ .../src/app/(authenticated)/layout.tsx | 7 +- .../api/stripe/connect/webhook/route.test.ts | 135 +++++++++++ .../app/api/stripe/connect/webhook/route.ts | 142 +++++++++++ .../components/navigation/top-navigation.tsx | 35 +++ .../components/stripe/stripe-connect-gate.tsx | 123 ++++++++++ apps/marketplace/src/lib/config.ts | 10 + apps/marketplace/src/lib/saleor/consts.ts | 4 + .../src/lib/saleor/vendor-page-metadata.ts | 131 ++++++++++ .../src/lib/saleor/vendor-payment-metadata.ts | 45 ++++ apps/marketplace/src/lib/stripe/connect.ts | 215 +++++++++++++++++ .../src/providers/auth-provider.tsx | 39 +++ 14 files changed, 1246 insertions(+), 1 deletion(-) create mode 100644 apps/marketplace/src/app/(authenticated)/_actions/stripe-connect.test.ts create mode 100644 apps/marketplace/src/app/(authenticated)/_actions/stripe-connect.ts create mode 100644 apps/marketplace/src/app/api/stripe/connect/webhook/route.test.ts create mode 100644 apps/marketplace/src/app/api/stripe/connect/webhook/route.ts create mode 100644 apps/marketplace/src/components/stripe/stripe-connect-gate.tsx create mode 100644 apps/marketplace/src/lib/saleor/vendor-page-metadata.ts create mode 100644 apps/marketplace/src/lib/saleor/vendor-payment-metadata.ts create mode 100644 apps/marketplace/src/lib/stripe/connect.ts diff --git a/.env.example b/.env.example index 0bb7bfac..8574a3dc 100644 --- a/.env.example +++ b/.env.example @@ -69,3 +69,7 @@ MARKETPLACE_SMTP_PASSWORD= MARKETPLACE_SMTP_SECURE=false MARKETPLACE_EMAIL_FROM= MARKETPLACE_SUPERADMIN_EMAIL= +# Stripe Connect (marketplace vendor onboarding) +MARKETPLACE_STRIPE_SECRET_KEY= +MARKETPLACE_STRIPE_CONNECT_WEBHOOK_SECRET= +MARKETPLACE_STRIPE_CONNECT_DEFAULT_COUNTRY=US diff --git a/apps/marketplace/src/app/(authenticated)/_actions/stripe-connect.test.ts b/apps/marketplace/src/app/(authenticated)/_actions/stripe-connect.test.ts new file mode 100644 index 00000000..f4c328e2 --- /dev/null +++ b/apps/marketplace/src/app/(authenticated)/_actions/stripe-connect.test.ts @@ -0,0 +1,131 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + createStripeConnectLoginSession, + createStripeConnectOnboardingSession, +} from "./stripe-connect"; + +const { + configurationServiceMock, + createStripeConnectAccountMock, + createStripeConnectLoginLinkMock, + createStripeConnectOnboardingLinkMock, + getServerAuthTokenMock, + getServerVendorIdMock, + getVendorPageMetadataMock, + resolveSaleorDomainFromTokenMock, + updateVendorPageMetadataMock, +} = vi.hoisted(() => ({ + getServerAuthTokenMock: vi.fn(), + getServerVendorIdMock: vi.fn(), + resolveSaleorDomainFromTokenMock: vi.fn(), + getVendorPageMetadataMock: vi.fn(), + updateVendorPageMetadataMock: vi.fn(), + createStripeConnectAccountMock: vi.fn(), + createStripeConnectOnboardingLinkMock: vi.fn(), + createStripeConnectLoginLinkMock: vi.fn(), + configurationServiceMock: { + getMe: vi.fn(), + }, +})); + +vi.mock("@/lib/auth/server", () => ({ + getServerAuthToken: getServerAuthTokenMock, + getServerVendorId: getServerVendorIdMock, +})); + +vi.mock("@/lib/saleor/vendor-page-metadata", () => ({ + resolveSaleorDomainFromToken: resolveSaleorDomainFromTokenMock, + getVendorPageMetadata: getVendorPageMetadataMock, + updateVendorPageMetadata: updateVendorPageMetadataMock, +})); + +vi.mock("@/lib/stripe/connect", () => ({ + createStripeConnectAccount: createStripeConnectAccountMock, + createStripeConnectOnboardingLink: createStripeConnectOnboardingLinkMock, + createStripeConnectLoginLink: createStripeConnectLoginLinkMock, + getStripeConnectAccount: vi.fn(), + isStripeConnectOnboardingCompleted: vi.fn(), +})); + +vi.mock("@/services/configuration", () => ({ + configurationService: configurationServiceMock, +})); + +describe("stripe connect actions", () => { + beforeEach(() => { + vi.clearAllMocks(); + getServerAuthTokenMock.mockResolvedValue("jwt-token"); + getServerVendorIdMock.mockResolvedValue("vendor-page-1"); + resolveSaleorDomainFromTokenMock.mockReturnValue("example.saleor.cloud"); + updateVendorPageMetadataMock.mockResolvedValue(undefined); + configurationServiceMock.getMe.mockResolvedValue({ + ok: true, + data: { me: { email: "vendor@example.com" } }, + }); + }); + + it("creates Stripe account and onboarding link when account id is missing", async () => { + getVendorPageMetadataMock.mockResolvedValue([]); + createStripeConnectAccountMock.mockResolvedValue({ id: "acct_123" }); + createStripeConnectOnboardingLinkMock.mockResolvedValue({ + url: "https://connect.stripe.com/onboarding-link", + }); + + const result = await createStripeConnectOnboardingSession(); + + expect(result.ok).toBe(true); + expect(createStripeConnectAccountMock).toHaveBeenCalledWith({ + email: "vendor@example.com", + saleorDomain: "example.saleor.cloud", + vendorId: "vendor-page-1", + }); + expect(updateVendorPageMetadataMock).toHaveBeenCalledTimes(1); + expect(createStripeConnectOnboardingLinkMock).toHaveBeenCalledWith({ + accountId: "acct_123", + refreshUrl: expect.stringContaining("/dashboard?stripe=refresh"), + returnUrl: expect.stringContaining("/dashboard?stripe=return"), + }); + }); + + it("reuses existing Stripe account id and only creates account link", async () => { + getVendorPageMetadataMock.mockResolvedValue([ + { key: "payment_account_id", value: "acct_existing" }, + { key: "payment_account_connected", value: "false" }, + ]); + createStripeConnectOnboardingLinkMock.mockResolvedValue({ + url: "https://connect.stripe.com/onboarding-link", + }); + + const result = await createStripeConnectOnboardingSession(); + + expect(result.ok).toBe(true); + expect(createStripeConnectAccountMock).not.toHaveBeenCalled(); + expect(updateVendorPageMetadataMock).not.toHaveBeenCalled(); + expect(createStripeConnectOnboardingLinkMock).toHaveBeenCalledWith({ + accountId: "acct_existing", + refreshUrl: expect.stringContaining("/dashboard?stripe=refresh"), + returnUrl: expect.stringContaining("/dashboard?stripe=return"), + }); + }); + + it("creates Stripe login link for configured vendor", async () => { + getVendorPageMetadataMock.mockResolvedValue([ + { key: "payment_account_id", value: "acct_existing" }, + { key: "payment_account_connected", value: "true" }, + ]); + createStripeConnectLoginLinkMock.mockResolvedValue({ + url: "https://connect.stripe.com/express/acct_existing", + }); + + const result = await createStripeConnectLoginSession(); + + expect(result).toEqual({ + ok: true, + url: "https://connect.stripe.com/express/acct_existing", + }); + expect(createStripeConnectLoginLinkMock).toHaveBeenCalledWith({ + accountId: "acct_existing", + }); + }); +}); diff --git a/apps/marketplace/src/app/(authenticated)/_actions/stripe-connect.ts b/apps/marketplace/src/app/(authenticated)/_actions/stripe-connect.ts new file mode 100644 index 00000000..3ae3af87 --- /dev/null +++ b/apps/marketplace/src/app/(authenticated)/_actions/stripe-connect.ts @@ -0,0 +1,226 @@ +"use server"; + +import { getServerAuthToken, getServerVendorId } from "@/lib/auth/server"; +import { config } from "@/lib/config"; +import { METADATA_KEYS } from "@/lib/saleor/consts"; +import { + getVendorPageMetadata, + resolveSaleorDomainFromToken, + updateVendorPageMetadata, +} from "@/lib/saleor/vendor-page-metadata"; +import { + getVendorPaymentMetadata, + mergeMetadata, +} from "@/lib/saleor/vendor-payment-metadata"; +import { + createStripeConnectAccount, + createStripeConnectLoginLink, + createStripeConnectOnboardingLink, + getStripeConnectAccount, + isStripeConnectOnboardingCompleted, +} from "@/lib/stripe/connect"; +import { configurationService } from "@/services/configuration"; + +type ActionResult = + | { + error: string; + ok: false; + } + | { + ok: true; + url: string; + }; + +type SyncResult = + | { + error: string; + ok: false; + } + | { + connected: boolean; + ok: true; + paymentAccountId: string; + }; + +async function getCurrentVendorContext() { + const token = await getServerAuthToken(); + + if (!token) { + throw new Error("Missing auth token"); + } + + const vendorPageId = await getServerVendorId(); + + if (!vendorPageId) { + throw new Error("Vendor profile not found"); + } + + const saleorDomain = resolveSaleorDomainFromToken(token); + + return { saleorDomain, token, vendorPageId }; +} + +function getStripeRedirectUrls() { + const baseUrl = config.urls.vendor.replace(/\/$/, ""); + const dashboardUrl = `${baseUrl}/dashboard`; + + return { + refreshUrl: `${dashboardUrl}?stripe=refresh`, + returnUrl: `${dashboardUrl}?stripe=return`, + }; +} + +export async function createStripeConnectOnboardingSession(): Promise { + try { + const { saleorDomain, token, vendorPageId } = + await getCurrentVendorContext(); + const vendorMetadata = await getVendorPageMetadata({ + saleorDomain, + vendorPageId, + }); + const paymentMetadata = getVendorPaymentMetadata(vendorMetadata); + + let paymentAccountId = paymentMetadata.paymentAccountId; + + if (!paymentAccountId) { + const meResult = await configurationService.getMe(token); + const email = + meResult.ok && meResult.data.me?.email + ? String(meResult.data.me.email) + : undefined; + const account = await createStripeConnectAccount({ + email, + saleorDomain, + vendorId: vendorPageId, + }); + + paymentAccountId = account.id; + await updateVendorPageMetadata({ + saleorDomain, + vendorPageId, + metadata: mergeMetadata(vendorMetadata, [ + { + key: METADATA_KEYS.PAYMENT_ACCOUNT_ID, + value: paymentAccountId, + }, + { + key: METADATA_KEYS.PAYMENT_ACCOUNT_CONNECTED, + value: "false", + }, + ]), + }); + } + + const redirectUrls = getStripeRedirectUrls(); + const accountLink = await createStripeConnectOnboardingLink({ + accountId: paymentAccountId, + refreshUrl: redirectUrls.refreshUrl, + returnUrl: redirectUrls.returnUrl, + }); + + return { + ok: true, + url: accountLink.url, + }; + } catch (error) { + console.error( + "[stripe-connect] Failed to create onboarding session", + error, + ); + + return { + ok: false, + error: + error instanceof Error + ? error.message + : "Failed to create Stripe onboarding session", + }; + } +} + +export async function createStripeConnectLoginSession(): Promise { + try { + const { saleorDomain, vendorPageId } = await getCurrentVendorContext(); + const vendorMetadata = await getVendorPageMetadata({ + saleorDomain, + vendorPageId, + }); + const paymentMetadata = getVendorPaymentMetadata(vendorMetadata); + + if (!paymentMetadata.paymentAccountId) { + return { + ok: false, + error: "Stripe account is not configured", + }; + } + + const loginLink = await createStripeConnectLoginLink({ + accountId: paymentMetadata.paymentAccountId, + }); + + return { + ok: true, + url: loginLink.url, + }; + } catch (error) { + console.error("[stripe-connect] Failed to create login session", error); + + return { + ok: false, + error: + error instanceof Error + ? error.message + : "Failed to create Stripe login session", + }; + } +} + +export async function syncStripeConnectStatus(): Promise { + try { + const { saleorDomain, vendorPageId } = await getCurrentVendorContext(); + const vendorMetadata = await getVendorPageMetadata({ + saleorDomain, + vendorPageId, + }); + const paymentMetadata = getVendorPaymentMetadata(vendorMetadata); + + if (!paymentMetadata.paymentAccountId) { + return { + ok: false, + error: "Stripe account is not configured", + }; + } + + const account = await getStripeConnectAccount({ + accountId: paymentMetadata.paymentAccountId, + }); + const connected = isStripeConnectOnboardingCompleted(account); + + await updateVendorPageMetadata({ + saleorDomain, + vendorPageId, + metadata: mergeMetadata(vendorMetadata, [ + { + key: METADATA_KEYS.PAYMENT_ACCOUNT_CONNECTED, + value: connected ? "true" : "false", + }, + ]), + }); + + return { + ok: true, + connected, + paymentAccountId: paymentMetadata.paymentAccountId, + }; + } catch (error) { + console.error("[stripe-connect] Failed to sync Stripe status", error); + + return { + ok: false, + error: + error instanceof Error + ? error.message + : "Failed to sync Stripe account status", + }; + } +} diff --git a/apps/marketplace/src/app/(authenticated)/layout.tsx b/apps/marketplace/src/app/(authenticated)/layout.tsx index 13ea0035..9d8e61d9 100644 --- a/apps/marketplace/src/app/(authenticated)/layout.tsx +++ b/apps/marketplace/src/app/(authenticated)/layout.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { type ReactNode, useEffect } from "react"; import { TopNavigation } from "@/components/navigation/top-navigation"; +import { StripeConnectGate } from "@/components/stripe/stripe-connect-gate"; import { useAuth } from "@/providers/auth-provider"; interface AuthenticatedLayoutProps { @@ -14,7 +15,7 @@ interface AuthenticatedLayoutProps { export default function AuthenticatedLayout({ children, }: AuthenticatedLayoutProps) { - const { isAuthenticated, isLoading } = useAuth(); + const { isAuthenticated, isLoading, stripeConnectRequired } = useAuth(); const router = useRouter(); useEffect(() => { @@ -35,6 +36,10 @@ export default function AuthenticatedLayout({ return null; } + if (stripeConnectRequired) { + return ; + } + return (
diff --git a/apps/marketplace/src/app/api/stripe/connect/webhook/route.test.ts b/apps/marketplace/src/app/api/stripe/connect/webhook/route.test.ts new file mode 100644 index 00000000..6d725d03 --- /dev/null +++ b/apps/marketplace/src/app/api/stripe/connect/webhook/route.test.ts @@ -0,0 +1,135 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { METADATA_KEYS } from "@/lib/saleor/consts"; + +import { POST } from "./route"; + +const { + getVendorPageMetadataMock, + isStripeConnectOnboardingCompletedMock, + updateVendorPageMetadataMock, + verifyStripeWebhookSignatureMock, +} = vi.hoisted(() => ({ + verifyStripeWebhookSignatureMock: vi.fn(), + isStripeConnectOnboardingCompletedMock: vi.fn(), + getVendorPageMetadataMock: vi.fn(), + updateVendorPageMetadataMock: vi.fn(), +})); + +vi.mock("@/lib/stripe/connect", () => ({ + verifyStripeWebhookSignature: verifyStripeWebhookSignatureMock, + isStripeConnectOnboardingCompleted: isStripeConnectOnboardingCompletedMock, +})); + +vi.mock("@/lib/saleor/vendor-page-metadata", () => ({ + getVendorPageMetadata: getVendorPageMetadataMock, + updateVendorPageMetadata: updateVendorPageMetadataMock, +})); + +function createRequest( + payload: unknown, + signature = "test-signature", +): Request { + return new Request("http://localhost/api/stripe/connect/webhook", { + method: "POST", + headers: { + "content-type": "application/json", + ...(signature ? { "stripe-signature": signature } : {}), + }, + body: JSON.stringify(payload), + }); +} + +describe("stripe connect webhook", () => { + beforeEach(() => { + vi.clearAllMocks(); + verifyStripeWebhookSignatureMock.mockReturnValue(true); + isStripeConnectOnboardingCompletedMock.mockReturnValue(true); + getVendorPageMetadataMock.mockResolvedValue([ + { key: "existing", value: "1" }, + ]); + updateVendorPageMetadataMock.mockResolvedValue(undefined); + process.env.NEXT_PUBLIC_SALEOR_URL = + "https://example.saleor.cloud/graphql/"; + }); + + it("updates vendor metadata for account.updated", async () => { + const response = await POST( + createRequest({ + id: "evt_1", + type: "account.updated", + data: { + object: { + id: "acct_123", + details_submitted: true, + requirements: { currently_due: [] }, + metadata: { + vendor_id: "vendor-page-1", + saleor_domain: "example.saleor.cloud", + }, + }, + }, + }), + ); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.status).toBe("processed"); + expect(getVendorPageMetadataMock).toHaveBeenCalledWith({ + saleorDomain: "example.saleor.cloud", + vendorPageId: "vendor-page-1", + }); + expect(updateVendorPageMetadataMock).toHaveBeenCalledTimes(1); + + const call = updateVendorPageMetadataMock.mock.calls[0]?.[0] as { + metadata: Array<{ key: string; value: string }>; + }; + const map = new Map(call.metadata.map((item) => [item.key, item.value])); + + expect(map.get(METADATA_KEYS.PAYMENT_ACCOUNT_ID)).toBe("acct_123"); + expect(map.get(METADATA_KEYS.PAYMENT_ACCOUNT_CONNECTED)).toBe("true"); + }); + + it("returns 400 for invalid signature", async () => { + verifyStripeWebhookSignatureMock.mockReturnValue(false); + + const response = await POST( + createRequest({ + type: "account.updated", + }), + ); + + expect(response.status).toBe(400); + expect(updateVendorPageMetadataMock).not.toHaveBeenCalled(); + }); + + it("skips event without vendor_id metadata", async () => { + const response = await POST( + createRequest({ + id: "evt_2", + type: "account.updated", + data: { + object: { + id: "acct_123", + metadata: {}, + }, + }, + }), + ); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.status).toBe("skipped"); + expect(data.reason).toBe("missing_vendor_id"); + expect(updateVendorPageMetadataMock).not.toHaveBeenCalled(); + }); + + it("returns 400 when stripe-signature is missing", async () => { + const response = await POST(createRequest({ type: "account.updated" }, "")); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.error).toContain("Missing stripe-signature"); + expect(verifyStripeWebhookSignatureMock).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/marketplace/src/app/api/stripe/connect/webhook/route.ts b/apps/marketplace/src/app/api/stripe/connect/webhook/route.ts new file mode 100644 index 00000000..90d98bd9 --- /dev/null +++ b/apps/marketplace/src/app/api/stripe/connect/webhook/route.ts @@ -0,0 +1,142 @@ +import { NextResponse } from "next/server"; + +import { METADATA_KEYS } from "@/lib/saleor/consts"; +import { + getVendorPageMetadata, + updateVendorPageMetadata, +} from "@/lib/saleor/vendor-page-metadata"; +import { mergeMetadata } from "@/lib/saleor/vendor-payment-metadata"; +import { + isStripeConnectOnboardingCompleted, + verifyStripeWebhookSignature, +} from "@/lib/stripe/connect"; + +type StripeAccountUpdatedEvent = { + data?: { + object?: { + details_submitted?: boolean; + id?: string; + metadata?: Record; + requirements?: { + currently_due?: string[]; + }; + }; + }; + id?: string; + type?: string; +}; + +function resolveDefaultSaleorDomain(): string | null { + const raw = process.env.NEXT_PUBLIC_SALEOR_URL; + + if (!raw) { + return null; + } + + try { + return new URL(raw).hostname; + } catch { + return null; + } +} + +export async function POST(request: Request) { + try { + const signature = request.headers.get("stripe-signature"); + + if (!signature) { + return NextResponse.json( + { error: "Missing stripe-signature header" }, + { status: 400 }, + ); + } + + const payload = await request.text(); + const verified = verifyStripeWebhookSignature({ payload, signature }); + + if (!verified) { + return NextResponse.json( + { error: "Invalid webhook signature" }, + { status: 400 }, + ); + } + + const event = JSON.parse(payload) as StripeAccountUpdatedEvent; + + if (event.type !== "account.updated") { + return NextResponse.json({ status: "ignored", type: event.type ?? null }); + } + + const account = event.data?.object; + + if (!account?.id) { + return NextResponse.json( + { error: "Missing account id in Stripe event" }, + { status: 400 }, + ); + } + + const vendorPageId = account.metadata?.vendor_id?.trim(); + + if (!vendorPageId) { + return NextResponse.json({ + status: "skipped", + reason: "missing_vendor_id", + }); + } + + const saleorDomainFromMetadata = account.metadata?.saleor_domain?.trim(); + const saleorDomain = + saleorDomainFromMetadata || resolveDefaultSaleorDomain(); + + if (!saleorDomain) { + return NextResponse.json( + { error: "Cannot resolve Saleor domain for webhook update" }, + { status: 500 }, + ); + } + + const connected = isStripeConnectOnboardingCompleted({ + details_submitted: account.details_submitted, + requirements: account.requirements, + }); + const currentMetadata = await getVendorPageMetadata({ + saleorDomain, + vendorPageId, + }); + + await updateVendorPageMetadata({ + saleorDomain, + vendorPageId, + metadata: mergeMetadata(currentMetadata, [ + { + key: METADATA_KEYS.PAYMENT_ACCOUNT_ID, + value: account.id, + }, + { + key: METADATA_KEYS.PAYMENT_ACCOUNT_CONNECTED, + value: connected ? "true" : "false", + }, + ]), + }); + + return NextResponse.json({ + status: "processed", + connected, + vendorPageId, + stripeAccountId: account.id, + }); + } catch (error) { + console.error("[stripe-connect] Failed to process webhook", error); + + return NextResponse.json( + { + error: + error instanceof Error + ? error.message + : "Failed to process Stripe webhook", + }, + { status: 500 }, + ); + } +} diff --git a/apps/marketplace/src/components/navigation/top-navigation.tsx b/apps/marketplace/src/components/navigation/top-navigation.tsx index e233bdd4..5b875dca 100644 --- a/apps/marketplace/src/components/navigation/top-navigation.tsx +++ b/apps/marketplace/src/components/navigation/top-navigation.tsx @@ -1,6 +1,7 @@ "use client"; import { + CircleDollarSign, FileText, LayoutDashboard, LayoutList, @@ -13,7 +14,9 @@ import { } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useState } from "react"; +import { createStripeConnectLoginSession } from "@/app/(authenticated)/_actions/stripe-connect"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { DropdownMenu, @@ -63,6 +66,7 @@ const navigationLinks = [ export function TopNavigation() { const { user, logout } = useAuth(); const pathname = usePathname(); + const [isOpeningStripe, setIsOpeningStripe] = useState(false); const isActive = (href: string) => { if (href === "/dashboard") { @@ -76,6 +80,29 @@ export function TopNavigation() { [user?.firstName, user?.lastName].filter(Boolean).join(" ") || user?.email || "Vendor"; + const hasStripeAccount = Boolean(user?.stripePaymentAccountId); + + const handleOpenStripe = async () => { + if (!hasStripeAccount || isOpeningStripe) { + return; + } + + setIsOpeningStripe(true); + + try { + const result = await createStripeConnectLoginSession(); + + if (!result.ok) { + console.error("[stripe-connect] Failed to open Stripe:", result.error); + + return; + } + + window.open(result.url, "_blank", "noopener,noreferrer"); + } finally { + setIsOpeningStripe(false); + } + }; return (
@@ -152,6 +179,14 @@ export function TopNavigation() { Configuration + void handleOpenStripe()} + disabled={!hasStripeAccount || isOpeningStripe} + > + + Go to Stripe + diff --git a/apps/marketplace/src/components/stripe/stripe-connect-gate.tsx b/apps/marketplace/src/components/stripe/stripe-connect-gate.tsx new file mode 100644 index 00000000..dac3ace2 --- /dev/null +++ b/apps/marketplace/src/components/stripe/stripe-connect-gate.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { Loader2 } from "lucide-react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; + +import { Button } from "@nimara/ui/components/button"; + +import { + createStripeConnectOnboardingSession, + syncStripeConnectStatus, +} from "@/app/(authenticated)/_actions/stripe-connect"; +import { useAuth } from "@/providers/auth-provider"; + +export function StripeConnectGate() { + const router = useRouter(); + const searchParams = useSearchParams(); + const { refreshVendorStripeState } = useAuth(); + const [isSettingUp, setIsSettingUp] = useState(false); + const [isSyncing, setIsSyncing] = useState(false); + const [error, setError] = useState(null); + const hasAutoSynced = useRef(false); + + const handleSetup = async () => { + setError(null); + setIsSettingUp(true); + + try { + const result = await createStripeConnectOnboardingSession(); + + if (!result.ok) { + setError(result.error); + + return; + } + + window.location.assign(result.url); + } finally { + setIsSettingUp(false); + } + }; + + const handleSync = async () => { + setError(null); + setIsSyncing(true); + + try { + const result = await syncStripeConnectStatus(); + + if (!result.ok) { + setError(result.error); + + return; + } + + await refreshVendorStripeState(); + router.replace("/dashboard"); + router.refresh(); + } finally { + setIsSyncing(false); + } + }; + + useEffect(() => { + const stripeState = searchParams.get("stripe"); + + if (!stripeState || hasAutoSynced.current) { + return; + } + + if (stripeState === "return") { + hasAutoSynced.current = true; + void handleSync(); + } + }, [searchParams]); + + return ( +
+
+
+

Stripe setup required

+

+ You need to finish Stripe Connect onboarding before using Vendor + Panel. +

+
+ + {error ? ( +
+ {error} +
+ ) : null} + +
+ + +
+
+
+ ); +} diff --git a/apps/marketplace/src/lib/config.ts b/apps/marketplace/src/lib/config.ts index adb03c63..5ea64a49 100644 --- a/apps/marketplace/src/lib/config.ts +++ b/apps/marketplace/src/lib/config.ts @@ -81,6 +81,11 @@ const envSchema = z.object({ MARKETPLACE_SMTP_SECURE: booleanFromEnv.default(false), MARKETPLACE_EMAIL_FROM: z.string().optional(), MARKETPLACE_SUPERADMIN_EMAIL: z.string().optional(), + + // Stripe Connect (marketplace vendor onboarding) + MARKETPLACE_STRIPE_SECRET_KEY: z.string().optional(), + MARKETPLACE_STRIPE_CONNECT_WEBHOOK_SECRET: z.string().optional(), + MARKETPLACE_STRIPE_CONNECT_DEFAULT_COUNTRY: z.string().default("US"), }); export type Env = z.infer; @@ -177,4 +182,9 @@ export const config = { from: env.MARKETPLACE_EMAIL_FROM, superadminEmail: env.MARKETPLACE_SUPERADMIN_EMAIL, }, + stripeConnect: { + secretKey: env.MARKETPLACE_STRIPE_SECRET_KEY, + webhookSecret: env.MARKETPLACE_STRIPE_CONNECT_WEBHOOK_SECRET, + defaultCountry: env.MARKETPLACE_STRIPE_CONNECT_DEFAULT_COUNTRY, + }, } as const; diff --git a/apps/marketplace/src/lib/saleor/consts.ts b/apps/marketplace/src/lib/saleor/consts.ts index 6e0c3753..e80354a1 100644 --- a/apps/marketplace/src/lib/saleor/consts.ts +++ b/apps/marketplace/src/lib/saleor/consts.ts @@ -9,6 +9,10 @@ export const METADATA_KEYS = { /** Vendor profile id – links Customer/Product/Order to vendor */ VENDOR_ID: "vendor.id", + /** Stripe Connect account id for vendor profile */ + PAYMENT_ACCOUNT_ID: "payment_account_id", + /** Stripe Connect onboarding completion flag for vendor profile */ + PAYMENT_ACCOUNT_CONNECTED: "payment_account_connected", /** Vendor-owned customer ids (stored in vendor page metadata as JSON array) */ VENDOR_CUSTOMERS: "meta.customers", /** Default collection flag (stored in collection metadata) */ diff --git a/apps/marketplace/src/lib/saleor/vendor-page-metadata.ts b/apps/marketplace/src/lib/saleor/vendor-page-metadata.ts new file mode 100644 index 00000000..c92dcf8c --- /dev/null +++ b/apps/marketplace/src/lib/saleor/vendor-page-metadata.ts @@ -0,0 +1,131 @@ +import { getAppConfig } from "@/lib/saleor/app-config"; +import { + getSaleorDomainFromToken, + getSaleorGraphQLEndpoint, +} from "@/lib/saleor/domain"; +import type { MetadataItem } from "@/lib/saleor/vendor-payment-metadata"; + +type GraphQLError = { + message?: string; +}; + +type GraphQLResponse = { + data?: TData; + errors?: GraphQLError[]; +}; + +function getErrorMessage(errors?: GraphQLError[]): string { + return ( + errors + ?.map((error) => error.message) + .filter(Boolean) + .join("; ") || "Saleor GraphQL operation failed" + ); +} + +async function executeSaleorAppGraphQL( + saleorDomain: string, + query: string, + variables: Record, +): Promise { + const appConfig = await getAppConfig(saleorDomain); + + if (!appConfig?.authToken) { + throw new Error( + `Missing app auth token for Saleor domain: ${saleorDomain}`, + ); + } + + const configData = appConfig.config; + const apiUrl = + typeof configData?.apiUrl === "string" + ? String(configData.apiUrl) + : getSaleorGraphQLEndpoint(saleorDomain); + + const response = await fetch(apiUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${appConfig.authToken}`, + }, + body: JSON.stringify({ query, variables }), + }); + + const body = (await response.json()) as GraphQLResponse; + + if (!response.ok) { + throw new Error(`Saleor GraphQL failed with status ${response.status}`); + } + + if (body.errors?.length) { + throw new Error(getErrorMessage(body.errors)); + } + + if (!body.data) { + throw new Error("Saleor GraphQL returned empty data"); + } + + return body.data; +} + +export function resolveSaleorDomainFromToken(token: string): string { + const saleorDomain = getSaleorDomainFromToken(token); + + if (!saleorDomain) { + throw new Error("Cannot resolve Saleor domain from token"); + } + + return saleorDomain; +} + +export async function getVendorPageMetadata(input: { + saleorDomain: string; + vendorPageId: string; +}): Promise { + const query = ` + query VendorPageMetadata($id: ID!) { + page(id: $id) { + id + metadata { + key + value + } + } + } + `; + + const data = await executeSaleorAppGraphQL<{ + page?: { metadata?: MetadataItem[] | null } | null; + }>(input.saleorDomain, query, { id: input.vendorPageId }); + + return data.page?.metadata ?? []; +} + +export async function updateVendorPageMetadata(input: { + metadata: MetadataItem[]; + saleorDomain: string; + vendorPageId: string; +}): Promise { + const mutation = ` + mutation UpdateVendorPageMetadata($id: ID!, $input: [MetadataInput!]!) { + updateMetadata(id: $id, input: $input) { + errors { + message + } + } + } + `; + + const data = await executeSaleorAppGraphQL<{ + updateMetadata?: { errors?: GraphQLError[] | null } | null; + }>(input.saleorDomain, mutation, { + id: input.vendorPageId, + input: input.metadata, + }); + + const errors = data.updateMetadata?.errors ?? []; + + if (errors.length > 0) { + throw new Error(getErrorMessage(errors)); + } +} diff --git a/apps/marketplace/src/lib/saleor/vendor-payment-metadata.ts b/apps/marketplace/src/lib/saleor/vendor-payment-metadata.ts new file mode 100644 index 00000000..0d40f462 --- /dev/null +++ b/apps/marketplace/src/lib/saleor/vendor-payment-metadata.ts @@ -0,0 +1,45 @@ +import { METADATA_KEYS } from "@/lib/saleor/consts"; + +export type MetadataItem = { + key: string; + value: string; +}; + +export type VendorPaymentMetadata = { + paymentAccountConnected: boolean; + paymentAccountId: string | null; +}; + +export function getVendorPaymentMetadata( + metadata: MetadataItem[] | null | undefined, +): VendorPaymentMetadata { + const metadataMap = new Map( + (metadata ?? []).map((item) => [item.key, item.value]), + ); + const paymentAccountId = + metadataMap.get(METADATA_KEYS.PAYMENT_ACCOUNT_ID)?.trim() ?? ""; + const rawConnected = + metadataMap.get(METADATA_KEYS.PAYMENT_ACCOUNT_CONNECTED) ?? ""; + const paymentAccountConnected = + rawConnected.trim().toLowerCase() === "true" || + rawConnected.trim() === "1" || + rawConnected.trim().toLowerCase() === "yes"; + + return { + paymentAccountId: paymentAccountId.length > 0 ? paymentAccountId : null, + paymentAccountConnected, + }; +} + +export function mergeMetadata( + existing: MetadataItem[] | null | undefined, + patch: MetadataItem[], +): MetadataItem[] { + const map = new Map((existing ?? []).map((item) => [item.key, item.value])); + + for (const item of patch) { + map.set(item.key, item.value); + } + + return Array.from(map.entries()).map(([key, value]) => ({ key, value })); +} diff --git a/apps/marketplace/src/lib/stripe/connect.ts b/apps/marketplace/src/lib/stripe/connect.ts new file mode 100644 index 00000000..3d242a22 --- /dev/null +++ b/apps/marketplace/src/lib/stripe/connect.ts @@ -0,0 +1,215 @@ +import crypto from "node:crypto"; + +import { config } from "@/lib/config"; + +type StripeApiError = { + error?: { + code?: string; + message?: string; + param?: string; + type?: string; + }; +}; + +type StripeRequirements = { + currently_due?: string[]; +}; + +export type StripeConnectAccount = { + details_submitted?: boolean; + id: string; + metadata?: Record; + requirements?: StripeRequirements; +}; + +function assertStripeSecretKey(): string { + const secretKey = config.stripeConnect.secretKey; + + if (!secretKey) { + throw new Error("MARKETPLACE_STRIPE_SECRET_KEY is not set"); + } + + return secretKey; +} + +function toFormBody( + payload: Record, +): URLSearchParams { + const params = new URLSearchParams(); + + for (const [key, value] of Object.entries(payload)) { + if (value == null) { + continue; + } + params.append(key, String(value)); + } + + return params; +} + +async function stripeRequest( + path: string, + payload: Record, +): Promise { + const secretKey = assertStripeSecretKey(); + const response = await fetch(`https://api.stripe.com${path}`, { + method: "POST", + headers: { + Authorization: `Bearer ${secretKey}`, + "Content-Type": "application/x-www-form-urlencoded", + }, + body: toFormBody(payload).toString(), + }); + + const body = (await response.json()) as StripeApiError & TResponse; + + if (!response.ok) { + const message = + body.error?.message ?? `Stripe API failed (${response.status})`; + + throw new Error(message); + } + + return body; +} + +export async function createStripeConnectAccount(input: { + email?: string; + saleorDomain?: string; + vendorId: string; +}): Promise { + return stripeRequest("/v1/accounts", { + country: config.stripeConnect.defaultCountry, + email: input.email, + "controller[fees][payer]": "application", + "controller[losses][payments]": "application", + "controller[stripe_dashboard][type]": "express", + "metadata[saleor_domain]": input.saleorDomain, + "metadata[vendor_id]": input.vendorId, + }); +} + +export async function createStripeConnectOnboardingLink(input: { + accountId: string; + refreshUrl: string; + returnUrl: string; +}): Promise<{ url: string }> { + return stripeRequest<{ url: string }>("/v1/account_links", { + account: input.accountId, + refresh_url: input.refreshUrl, + return_url: input.returnUrl, + type: "account_onboarding", + }); +} + +export async function createStripeConnectLoginLink(input: { + accountId: string; +}): Promise<{ url: string }> { + return stripeRequest<{ url: string }>( + `/v1/accounts/${input.accountId}/login_links`, + {}, + ); +} + +export async function getStripeConnectAccount(input: { + accountId: string; +}): Promise { + const secretKey = assertStripeSecretKey(); + const response = await fetch( + `https://api.stripe.com/v1/accounts/${input.accountId}`, + { + method: "GET", + headers: { + Authorization: `Bearer ${secretKey}`, + }, + }, + ); + + const body = (await response.json()) as StripeApiError & StripeConnectAccount; + + if (!response.ok) { + const message = + body.error?.message ?? `Stripe API failed (${response.status})`; + + throw new Error(message); + } + + return body; +} + +export function isStripeConnectOnboardingCompleted( + account: Pick, +): boolean { + const currentlyDue = account.requirements?.currently_due ?? []; + + return Boolean(account.details_submitted) && currentlyDue.length === 0; +} + +function parseStripeSignature(signature: string): { + timestamp: string; + v1Signatures: string[]; +} | null { + const chunks = signature.split(",").map((value) => value.trim()); + const timestamp = chunks + .find((value) => value.startsWith("t=")) + ?.replace(/^t=/, ""); + const v1Signatures = chunks + .filter((value) => value.startsWith("v1=")) + .map((value) => value.replace(/^v1=/, "")); + + if (!timestamp || v1Signatures.length === 0) { + return null; + } + + return { timestamp, v1Signatures }; +} + +export function verifyStripeWebhookSignature(input: { + payload: string; + signature: string; + toleranceSeconds?: number; + webhookSecret?: string; +}): boolean { + const webhookSecret = + input.webhookSecret ?? config.stripeConnect.webhookSecret; + + if (!webhookSecret) { + throw new Error("MARKETPLACE_STRIPE_CONNECT_WEBHOOK_SECRET is not set"); + } + + const parsed = parseStripeSignature(input.signature); + + if (!parsed) { + return false; + } + + const tolerance = input.toleranceSeconds ?? 300; + const timestampMs = Number(parsed.timestamp) * 1000; + + if (!Number.isFinite(timestampMs)) { + return false; + } + + const now = Date.now(); + + if (Math.abs(now - timestampMs) > tolerance * 1000) { + return false; + } + + const signedPayload = `${parsed.timestamp}.${input.payload}`; + const expectedSignature = crypto + .createHmac("sha256", webhookSecret) + .update(signedPayload, "utf8") + .digest("hex"); + + return parsed.v1Signatures.some((candidate) => { + const expectedBuffer = Buffer.from(expectedSignature, "hex"); + const candidateBuffer = Buffer.from(candidate, "hex"); + + if (expectedBuffer.length !== candidateBuffer.length) { + return false; + } + + return crypto.timingSafeEqual(expectedBuffer, candidateBuffer); + }); +} diff --git a/apps/marketplace/src/providers/auth-provider.tsx b/apps/marketplace/src/providers/auth-provider.tsx index c6630157..c4dd5c66 100644 --- a/apps/marketplace/src/providers/auth-provider.tsx +++ b/apps/marketplace/src/providers/auth-provider.tsx @@ -19,6 +19,7 @@ import { initDomainFromUrl, setAppBridgeDomain, } from "@/lib/saleor/app-bridge-domain"; +import { getVendorPaymentMetadata } from "@/lib/saleor/vendor-payment-metadata"; // Storage keys for auth tokens const AUTH_ACCESS_TOKEN_KEY = "auth_token"; @@ -31,6 +32,8 @@ interface User { firstName?: string; id: string; lastName?: string; + stripePaymentAccountConnected?: boolean; + stripePaymentAccountId?: string | null; vendorId?: string; } @@ -42,7 +45,9 @@ interface AuthContextType { login: (email: string, password: string) => Promise; logout: () => Promise; refreshAccessToken: () => Promise; + refreshVendorStripeState: () => Promise; setToken: (token: string) => void; + stripeConnectRequired: boolean; token: string | null; user: User | null; } @@ -263,6 +268,10 @@ export function AuthProvider({ query VendorPageStatus($id: ID!) { page(id: $id) { id + metadata { + key + value + } attributes { attribute { slug } values { name } @@ -281,6 +290,7 @@ export function AuthProvider({ attribute?: { slug?: string | null } | null; values?: Array<{ name?: string | null } | null> | null; }> | null; + metadata?: Array<{ key: string; value: string }> | null; } | null; }; errors?: Array<{ message?: unknown }>; @@ -339,10 +349,26 @@ export function AuthProvider({ statusAttr?.values?.[0]?.name != null ? String(statusAttr.values[0]?.name) : ""; + const paymentMetadata = getVendorPaymentMetadata( + vendorData?.data?.page?.metadata, + ); if (statusValue !== "active") { throw new Error("Your account is not yet active."); } + + setUser({ + email: me.email ?? "", + firstName: me.firstName, + id: me.id, + lastName: me.lastName, + stripePaymentAccountConnected: + paymentMetadata.paymentAccountConnected, + stripePaymentAccountId: paymentMetadata.paymentAccountId, + ...(vendorPageId ? { vendorId: vendorPageId } : {}), + }); + + return true; } setUser({ @@ -699,6 +725,14 @@ export function AuthProvider({ router.push("/sign-in"); }, [clearAuth, router]); + const refreshVendorStripeState = useCallback(async () => { + if (!token) { + return; + } + + await fetchUser(token); + }, [fetchUser, token]); + // Periodic token refresh - check every minute if token needs refresh useEffect(() => { if (!token || !user) { @@ -734,6 +768,9 @@ export function AuthProvider({ // Authenticated: Saleor Cloud user token (App Bridge/login) OR dashboard context (saleorApiUrl in URL) const isAuthenticated = !!token || (dashboardContext && !!getAppBridgeDomain()); + const stripeConnectRequired = + Boolean(user?.vendorId) && + (!user?.stripePaymentAccountId || !user?.stripePaymentAccountConnected); return ( Date: Fri, 13 Mar 2026 08:50:31 +0100 Subject: [PATCH 02/23] feat: Fix syntax --- apps/marketplace/next-env.d.ts | 2 +- apps/marketplace/src/providers/auth-provider.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/marketplace/next-env.d.ts b/apps/marketplace/next-env.d.ts index c4b7818f..9edff1c7 100644 --- a/apps/marketplace/next-env.d.ts +++ b/apps/marketplace/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/marketplace/src/providers/auth-provider.tsx b/apps/marketplace/src/providers/auth-provider.tsx index dce4f91d..29c1912a 100644 --- a/apps/marketplace/src/providers/auth-provider.tsx +++ b/apps/marketplace/src/providers/auth-provider.tsx @@ -354,7 +354,7 @@ export function AuthProvider({ throw new Error(t("error-account-not-active")); } - const paymentMetadata = getVendorPaymentMetadata( + paymentMetadata = getVendorPaymentMetadata( vendorData?.data?.page?.metadata, ); } From feaa9d3c346f83004cf079078186f05f1d70215c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bogda=C5=82?= Date: Mon, 16 Mar 2026 10:36:41 +0100 Subject: [PATCH 03/23] feat: Add translations --- .../components/navigation/top-navigation.tsx | 62 ++++++------------- .../components/stripe/stripe-connect-gate.tsx | 17 +++-- .../i18n/src/messages/en/marketplace.json | 24 +++++++ 3 files changed, 51 insertions(+), 52 deletions(-) diff --git a/apps/marketplace/src/components/navigation/top-navigation.tsx b/apps/marketplace/src/components/navigation/top-navigation.tsx index 5b875dca..9a54835d 100644 --- a/apps/marketplace/src/components/navigation/top-navigation.tsx +++ b/apps/marketplace/src/components/navigation/top-navigation.tsx @@ -13,6 +13,7 @@ import { Users, } from "lucide-react"; import Link from "next/link"; +import { useTranslations } from "next-intl"; import { usePathname } from "next/navigation"; import { useState } from "react"; @@ -28,42 +29,17 @@ import { import { cn } from "@/lib/utils"; import { useAuth } from "@/providers/auth-provider"; -const APP_NAME = "Vendor Panel"; - const navigationLinks = [ - { - name: "Dashboard", - href: "/dashboard", - icon: LayoutDashboard, - }, - { - name: "Products", - href: "/products", - icon: Package, - }, - { - name: "Orders", - href: "/orders", - icon: ShoppingCart, - }, - { - name: "Drafts", - href: "/drafts", - icon: FileText, - }, - { - name: "Collections", - href: "/collections", - icon: LayoutList, - }, - { - name: "Customers", - href: "/customers", - icon: Users, - }, -]; + { key: "dashboard", href: "/dashboard", icon: LayoutDashboard }, + { key: "products", href: "/products", icon: Package }, + { key: "orders", href: "/orders", icon: ShoppingCart }, + { key: "drafts", href: "/drafts", icon: FileText }, + { key: "collections", href: "/collections", icon: LayoutList }, + { key: "customers", href: "/customers", icon: Users }, +] as const; export function TopNavigation() { + const t = useTranslations("marketplace.navigation"); const { user, logout } = useAuth(); const pathname = usePathname(); const [isOpeningStripe, setIsOpeningStripe] = useState(false); @@ -79,7 +55,7 @@ export function TopNavigation() { const vendorName = [user?.firstName, user?.lastName].filter(Boolean).join(" ") || user?.email || - "Vendor"; + t("vendor-fallback"); const hasStripeAccount = Boolean(user?.stripePaymentAccountId); const handleOpenStripe = async () => { @@ -110,7 +86,7 @@ export function TopNavigation() { {/* Left side - Brand and Navigation */}
- {APP_NAME} + {t("app-name")}
- {item.name} + {t(item.key)} ); })} @@ -162,13 +138,13 @@ export function TopNavigation() {
- Invite co-workers + {t("invite-coworkers")} ? - Support + {t("support")} - Configuration + {t("configuration")} - Go to Stripe + {t("go-to-stripe")} - Sign out from other devices + {t("sign-out-devices")} logout()} className="cursor-pointer gap-2" > - Sign out + {t("sign-out")} diff --git a/apps/marketplace/src/components/stripe/stripe-connect-gate.tsx b/apps/marketplace/src/components/stripe/stripe-connect-gate.tsx index dac3ace2..ff52e187 100644 --- a/apps/marketplace/src/components/stripe/stripe-connect-gate.tsx +++ b/apps/marketplace/src/components/stripe/stripe-connect-gate.tsx @@ -1,6 +1,7 @@ "use client"; import { Loader2 } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useRef, useState } from "react"; @@ -13,6 +14,7 @@ import { import { useAuth } from "@/providers/auth-provider"; export function StripeConnectGate() { + const t = useTranslations("marketplace.stripeConnect"); const router = useRouter(); const searchParams = useSearchParams(); const { refreshVendorStripeState } = useAuth(); @@ -78,11 +80,8 @@ export function StripeConnectGate() {
-

Stripe setup required

-

- You need to finish Stripe Connect onboarding before using Vendor - Panel. -

+

{t("title")}

+

{t("description")}

{error ? ( @@ -96,10 +95,10 @@ export function StripeConnectGate() { {isSettingUp ? ( - Redirecting... + {t("redirecting")} ) : ( - "Setup" + t("setup-button") )}
diff --git a/packages/i18n/src/messages/en/marketplace.json b/packages/i18n/src/messages/en/marketplace.json index b2fc4dd3..f71a38ee 100644 --- a/packages/i18n/src/messages/en/marketplace.json +++ b/packages/i18n/src/messages/en/marketplace.json @@ -221,6 +221,14 @@ "dashboard": { "title": "Dashboard" }, + "stripeConnect": { + "title": "Stripe setup required", + "description": "You need to finish Stripe Connect onboarding before using Vendor Panel.", + "redirecting": "Redirecting...", + "setup-button": "Setup", + "checking": "Checking...", + "completed-setup-button": "I've completed setup" + }, "drafts": { "list": { "empty-list": "No drafts found", @@ -234,6 +242,22 @@ "description": "Manage your marketplace vendor account", "title": "Marketplace Vendor Panel" }, + "navigation": { + "app-name": "Vendor Panel", + "dashboard": "Dashboard", + "products": "Products", + "orders": "Orders", + "drafts": "Drafts", + "collections": "Collections", + "customers": "Customers", + "vendor-fallback": "Vendor", + "invite-coworkers": "Invite co-workers", + "support": "Support", + "configuration": "Configuration", + "go-to-stripe": "Go to Stripe", + "sign-out-devices": "Sign out from other devices", + "sign-out": "Sign out" + }, "orders": { "detail": { "add-discount": "Add discount", From df3c64bf2d577782a6496e57c528e08561145171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bogda=C5=82?= Date: Mon, 16 Mar 2026 10:43:55 +0100 Subject: [PATCH 04/23] fix: Linter warnings --- apps/marketplace/src/components/navigation/top-navigation.tsx | 2 +- apps/marketplace/src/components/stripe/stripe-connect-gate.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/marketplace/src/components/navigation/top-navigation.tsx b/apps/marketplace/src/components/navigation/top-navigation.tsx index 9a54835d..7dbe395f 100644 --- a/apps/marketplace/src/components/navigation/top-navigation.tsx +++ b/apps/marketplace/src/components/navigation/top-navigation.tsx @@ -13,8 +13,8 @@ import { Users, } from "lucide-react"; import Link from "next/link"; -import { useTranslations } from "next-intl"; import { usePathname } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useState } from "react"; import { createStripeConnectLoginSession } from "@/app/(authenticated)/_actions/stripe-connect"; diff --git a/apps/marketplace/src/components/stripe/stripe-connect-gate.tsx b/apps/marketplace/src/components/stripe/stripe-connect-gate.tsx index ff52e187..9e1b3a48 100644 --- a/apps/marketplace/src/components/stripe/stripe-connect-gate.tsx +++ b/apps/marketplace/src/components/stripe/stripe-connect-gate.tsx @@ -1,8 +1,8 @@ "use client"; import { Loader2 } from "lucide-react"; -import { useTranslations } from "next-intl"; import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useEffect, useRef, useState } from "react"; import { Button } from "@nimara/ui/components/button"; From 8a7490b3d72b49de4bc871d4af15de6f84899a2b Mon Sep 17 00:00:00 2001 From: Tomasz Stuba Date: Sun, 22 Mar 2026 19:07:00 +0100 Subject: [PATCH 05/23] feat: working on payment flow --- .env.example | 1 + .../app/api/payments/payment-intent/route.ts | 278 ++++++++++++++ .../app/api/payments/stripe/webhooks/route.ts | 356 ++++++++++++++++++ .../src/graphql/generated/client.ts | 100 +++-- .../mutations/TransactionCreate.graphql | 21 ++ .../queries/CheckoutTransactions.graphql | 12 + .../src/lib/graphql/server/auth.ts | 2 + .../src/lib/graphql/server/schema.ts | 2 + apps/marketplace/src/lib/stripe/client.ts | 168 +++++++++ apps/marketplace/src/lib/stripe/currency.ts | 51 +++ .../src/lib/stripe/webhook-signature.ts | 71 ++++ apps/marketplace/src/services/transactions.ts | 35 ++ 12 files changed, 1070 insertions(+), 27 deletions(-) create mode 100644 apps/marketplace/src/app/api/payments/payment-intent/route.ts create mode 100644 apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts create mode 100644 apps/marketplace/src/graphql/mutations/TransactionCreate.graphql create mode 100644 apps/marketplace/src/graphql/queries/CheckoutTransactions.graphql create mode 100644 apps/marketplace/src/lib/stripe/client.ts create mode 100644 apps/marketplace/src/lib/stripe/currency.ts create mode 100644 apps/marketplace/src/lib/stripe/webhook-signature.ts create mode 100644 apps/marketplace/src/services/transactions.ts diff --git a/.env.example b/.env.example index 0bb7bfac..7742cb69 100644 --- a/.env.example +++ b/.env.example @@ -55,6 +55,7 @@ VERCEL_ACCESS_TOKEN= VERCEL_TEAM_ID= VERCEL_EDGE_CONFIG_ID= MARKETPLACE_APP_CONFIG_EDGE_KEY=marketplace-app-config +STRIPE_WEBHOOK_SIGNING_SECRET= # MARKETPLACE_EDGE_CONFIG= NEXT_PUBLIC_MARKETPLACE_VENDOR_URL=http://localhost:3001 NEXT_PUBLIC_MARKETPLACE_STOREFRONT_URL=http://localhost:3000 diff --git a/apps/marketplace/src/app/api/payments/payment-intent/route.ts b/apps/marketplace/src/app/api/payments/payment-intent/route.ts new file mode 100644 index 00000000..8179ecd3 --- /dev/null +++ b/apps/marketplace/src/app/api/payments/payment-intent/route.ts @@ -0,0 +1,278 @@ +import { createHash, randomUUID } from "crypto"; +import { type NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import type { TransactionCreateVariables } from "@/graphql/generated/client"; +import { getAppConfig } from "@/lib/saleor/app-config"; +import { getServerAuthToken } from "@/lib/auth/server"; +import { getCentsFromAmount } from "@/lib/stripe/currency"; +import { getStripeClient } from "@/lib/stripe/client"; +import { transactionsService } from "@/services/transactions"; + +const bodySchema = z + .object({ + buyerId: z.string().optional(), + checkouts: z + .array( + z.object({ + checkoutId: z.string().min(1), + amount: z.number().positive(), + currency: z.string().length(3), + }), + ) + .min(1), + }) + .superRefine((data, ctx) => { + const uniqueIds = new Set( + data.checkouts.map((checkout) => checkout.checkoutId), + ); + + if (uniqueIds.size !== data.checkouts.length) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["checkouts"], + message: "checkoutId must be unique in checkouts array.", + }); + } + }); + +type FailedTransactionCreate = { + errors: Array<{ code: string; message: string }>; + checkoutId: string; +}; + +type TransactionCreatePayload = { + errors: Array<{ code: string; message: string | null }>; + transaction: { id: string; name: string } | null; +}; + +const buildIdempotencyKey = ( + checkouts: { amount: number; currency: string; checkoutId: string }[], +) => { + const canonical = [...checkouts] + .sort((a, b) => a.checkoutId.localeCompare(b.checkoutId)) + .map((checkout) => ({ + checkoutId: checkout.checkoutId, + amount: checkout.amount, + currency: checkout.currency.toUpperCase(), + })); + + const hash = createHash("sha256") + .update(JSON.stringify(canonical), "utf8") + .digest("hex"); + + return `pi-${hash}`; +}; + +export async function POST(request: NextRequest) { + const saleorDomain = request.headers.get("x-saleor-domain"); + + if (!saleorDomain) { + return NextResponse.json( + { error: "Missing x-saleor-domain header" }, + { status: 400 }, + ); + } + + const bodyParsed = bodySchema.safeParse(await request.json()); + + if (!bodyParsed.success) { + return NextResponse.json( + { error: "Invalid body", details: bodyParsed.error.flatten() }, + { status: 400 }, + ); + } + + const { checkouts, buyerId } = bodyParsed.data; + const currencies = Array.from( + new Set(checkouts.map((checkout) => checkout.currency.toUpperCase())), + ); + + if (currencies.length > 1) { + return NextResponse.json( + { + error: "Checkouts use mixed currencies.", + }, + { status: 422 }, + ); + } + + const config = await getAppConfig(saleorDomain); + + if (!config) { + return NextResponse.json( + { + error: `Missing app config for domain ${saleorDomain}`, + }, + { status: 404 }, + ); + } + + const currency = currencies[0]!; + const amount = checkouts.reduce( + (sum, item) => + sum + + getCentsFromAmount({ amount: item.amount, currency: item.currency }), + 0, + ); + const transferGroup = `tg_${Date.now()}_${randomUUID()}`; + + try { + const stripe = getStripeClient(); + const paymentIntent = await stripe.paymentIntents.create({ + amount, + currency: currency.toLowerCase(), + automatic_payment_methods: { enabled: true }, + transfer_group: transferGroup, + idempotencyKey: buildIdempotencyKey(checkouts), + metadata: { + subcheckouts: JSON.stringify(checkouts.map((item) => item.checkoutId)), + checkout_amounts: JSON.stringify( + Object.fromEntries( + checkouts.map((item) => [item.checkoutId, item.amount]), + ), + ), + saleor_domain: saleorDomain, + buyer_id: buyerId ?? "", + marketplace_model: "separate_charges_transfers", + }, + }); + const token = await getServerAuthToken(); + + const transactionCreateSettled = await Promise.allSettled( + checkouts.map((checkout) => { + const transactionVariables: TransactionCreateVariables = { + id: checkout.checkoutId, + transaction: { + name: "PaymentIntent created", + amountAuthorized: { + amount: checkout.amount, + currency: checkout.currency.toUpperCase(), + }, + pspReference: paymentIntent.id, + }, + transactionEvent: { + pspReference: paymentIntent.id, + message: "Waiting for customer action", + }, + }; + + return transactionsService.createTransaction( + transactionVariables, + token, + ); + }), + ); + + const failedTransactionCreates: FailedTransactionCreate[] = []; + let createdTransactionsCount = 0; + + transactionCreateSettled.forEach((entry, index) => { + const checkoutId = checkouts[index]!.checkoutId; + + if (entry.status === "rejected") { + failedTransactionCreates.push({ + checkoutId, + errors: [ + { + code: "REQUEST_FAILED", + message: + entry.reason instanceof Error + ? entry.reason.message + : "transactionCreate failed.", + }, + ], + }); + + return; + } + + if (!entry.value.ok) { + failedTransactionCreates.push({ + checkoutId, + errors: entry.value.errors.map((error) => ({ + code: error.code, + message: error.message ?? "transactionCreate failed.", + })), + }); + + return; + } + + const transactionCreateResultData = entry.value.data as + | TransactionCreatePayload + | { transactionCreate: TransactionCreatePayload | null }; + const transactionCreateResult = + "transactionCreate" in transactionCreateResultData + ? transactionCreateResultData.transactionCreate + : transactionCreateResultData; + + if ( + !transactionCreateResult?.transaction || + transactionCreateResult.errors.length > 0 + ) { + const mappedErrors = transactionCreateResult?.errors.map((error) => ({ + code: error.code, + message: error.message ?? "Unknown transactionCreate error.", + })); + + failedTransactionCreates.push({ + checkoutId, + errors: + mappedErrors && mappedErrors.length > 0 + ? mappedErrors + : [ + { + code: "UNKNOWN_TRANSACTION_CREATE_ERROR", + message: + "transactionCreate returned no transaction and no error details.", + }, + ], + }); + + return; + } + + createdTransactionsCount += 1; + }); + + const hasAtLeastOneCreated = createdTransactionsCount > 0; + const hasFailures = failedTransactionCreates.length > 0; + + if (hasFailures) { + const errorResponse = { + error: "Failed to create transaction for one or more orders.", + paymentIntentId: paymentIntent.id, + transferGroup, + failedTransactionCreates, + }; + + if (hasAtLeastOneCreated) { + return NextResponse.json(errorResponse, { status: 200 }); + } + + return NextResponse.json(errorResponse, { status: 500 }); + } + + return NextResponse.json( + { + clientSecret: paymentIntent.client_secret, + paymentIntentId: paymentIntent.id, + transferGroup, + currency: currency.toLowerCase(), + amount, + checkouts, + }, + { status: 200 }, + ); + } catch (error) { + return NextResponse.json( + { + error: "Failed to create Stripe PaymentIntent", + details: + error instanceof Error ? { message: error.message } : undefined, + }, + { status: 500 }, + ); + } +} diff --git a/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts b/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts new file mode 100644 index 00000000..d3ab5a05 --- /dev/null +++ b/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts @@ -0,0 +1,356 @@ +import { type NextRequest, NextResponse } from "next/server"; + +import type { TransactionCreateVariables } from "@/graphql/generated/client"; +import { getAppConfig } from "@/lib/saleor/app-config"; +import { verifyStripeWebhookSignature } from "@/lib/stripe/webhook-signature"; +import { transactionsService } from "@/services/transactions"; + +type StripePaymentIntentSucceededEvent = { + data?: { + object?: { + currency?: string; + id?: string; + metadata?: { + checkout_amounts?: string; + saleor_domain?: string; + subcheckouts?: string; + }; + }; + }; + id?: string; + type?: string; +}; + +type FailedTransactionCreate = { + checkoutId: string; + errors: Array<{ code: string; message: string }>; +}; + +type TransactionCreatePayload = { + errors: Array<{ code: string; message: string | null }>; + transaction: { id: string; name: string } | null; +}; + +type CheckoutTransactionsPayload = { + checkout: { + transactions: Array<{ + chargedAmount: { amount: number } | null; + pspReference: string | null; + }> | null; + } | null; +}; + +const getSaleorDomainFromEnv = () => { + const saleorUrl = process.env.NEXT_PUBLIC_SALEOR_URL; + + if (!saleorUrl) { + return null; + } + + try { + return new URL(saleorUrl).hostname; + } catch { + return null; + } +}; + +export async function POST(request: NextRequest) { + const stripeSignature = request.headers.get("stripe-signature"); + + if (!stripeSignature) { + return NextResponse.json( + { error: "Missing stripe-signature header." }, + { status: 400 }, + ); + } + + const rawPayload = await request.text(); + const isValidSignature = verifyStripeWebhookSignature({ + payload: rawPayload, + stripeSignature, + }); + + if (!isValidSignature) { + return NextResponse.json( + { error: "Invalid Stripe webhook signature." }, + { status: 400 }, + ); + } + + let event: StripePaymentIntentSucceededEvent; + + try { + event = JSON.parse(rawPayload) as StripePaymentIntentSucceededEvent; + } catch { + return NextResponse.json( + { error: "Invalid Stripe payload." }, + { status: 400 }, + ); + } + + if (event.type !== "payment_intent.succeeded") { + return NextResponse.json({ status: "skipped" }, { status: 200 }); + } + + const paymentIntent = event.data?.object; + const paymentIntentId = paymentIntent?.id; + const currency = paymentIntent?.currency?.toUpperCase(); + const checkoutIdsRaw = paymentIntent?.metadata?.subcheckouts; + const checkoutAmountsRaw = paymentIntent?.metadata?.checkout_amounts; + + if (!paymentIntentId || !currency || !checkoutIdsRaw || !checkoutAmountsRaw) { + return NextResponse.json( + { error: "Missing required PaymentIntent metadata." }, + { status: 500 }, + ); + } + + const saleorDomain = + paymentIntent.metadata?.saleor_domain ?? getSaleorDomainFromEnv(); + + if (!saleorDomain) { + return NextResponse.json( + { + error: + "Missing saleor domain in PaymentIntent metadata and NEXT_PUBLIC_SALEOR_URL.", + }, + { status: 500 }, + ); + } + + let checkoutIds: string[]; + let checkoutAmounts: Record; + + try { + const parsedCheckoutIds = JSON.parse(checkoutIdsRaw) as unknown; + const parsedCheckoutAmounts = JSON.parse(checkoutAmountsRaw) as unknown; + + if (!Array.isArray(parsedCheckoutIds)) { + throw new Error("subcheckouts should be an array"); + } + + checkoutIds = parsedCheckoutIds.map((id) => String(id)); + + if ( + typeof parsedCheckoutAmounts !== "object" || + parsedCheckoutAmounts === null || + Array.isArray(parsedCheckoutAmounts) + ) { + throw new Error("checkout_amounts should be an object"); + } + + const checkoutAmountsRecord = parsedCheckoutAmounts as Record< + string, + unknown + >; + + checkoutAmounts = Object.fromEntries( + Object.entries(checkoutAmountsRecord).map(([checkoutId, amount]) => [ + checkoutId, + Number(amount), + ]), + ); + } catch { + return NextResponse.json( + { error: "Invalid PaymentIntent metadata payload." }, + { status: 500 }, + ); + } + + const invalidCheckoutAmount = checkoutIds.find((checkoutId) => { + const amount = checkoutAmounts[checkoutId]; + + return !Number.isFinite(amount); + }); + + if (invalidCheckoutAmount) { + return NextResponse.json( + { + error: `Missing/invalid checkout amount for ${invalidCheckoutAmount}.`, + }, + { status: 500 }, + ); + } + + const config = await getAppConfig(saleorDomain); + + if (!config) { + return NextResponse.json( + { error: `Missing app config for domain ${saleorDomain}` }, + { status: 500 }, + ); + } + + const settled = await Promise.allSettled( + checkoutIds.map(async (checkoutId) => { + const amount = checkoutAmounts[checkoutId]; + const checkoutTransactionsResult = + await transactionsService.getCheckoutTransactions( + { id: checkoutId }, + config.authToken, + ); + + if (!checkoutTransactionsResult.ok) { + return { + transaction: null, + errors: checkoutTransactionsResult.errors.map((error) => ({ + code: error.code, + message: error.message ?? "checkout query failed.", + })), + }; + } + + const checkoutTransactionsData = + checkoutTransactionsResult.data as CheckoutTransactionsPayload; + const alreadyCharged = + checkoutTransactionsData.checkout?.transactions?.some((transaction) => { + if (transaction.pspReference !== paymentIntentId) { + return false; + } + + return (transaction.chargedAmount?.amount ?? 0) > 0; + }) ?? false; + + if (alreadyCharged) { + return { + transaction: { + id: "already-processed", + name: "PaymentIntent Succeeded", + }, + errors: [], + }; + } + + const transactionVariables: TransactionCreateVariables = { + id: checkoutId, + transaction: { + name: "PaymentIntent Succeeded", + amountCharged: { + amount, + currency, + }, + pspReference: paymentIntentId, + }, + transactionEvent: { + pspReference: paymentIntentId, + message: "Payment successful", + }, + }; + const transactionCreateResult = + await transactionsService.createTransaction( + transactionVariables, + config.authToken, + ); + + // checkotu complete here + + if (!transactionCreateResult.ok) { + return { + transaction: null, + errors: transactionCreateResult.errors.map((error) => ({ + code: error.code, + message: error.message ?? "transactionCreate failed.", + })), + }; + } + + const transactionCreateResultData = transactionCreateResult.data as + | TransactionCreatePayload + | { transactionCreate: TransactionCreatePayload | null }; + const transactionCreatePayload = + "transactionCreate" in transactionCreateResultData + ? transactionCreateResultData.transactionCreate + : transactionCreateResultData; + + if ( + !transactionCreatePayload?.transaction || + transactionCreatePayload.errors.length > 0 + ) { + const mappedErrors = transactionCreatePayload?.errors.map((error) => ({ + code: error.code, + message: error.message ?? "Unknown transactionCreate error.", + })); + + return { + transaction: null, + errors: + mappedErrors && mappedErrors.length > 0 + ? mappedErrors + : [ + { + code: "UNKNOWN_TRANSACTION_CREATE_ERROR", + message: + "transactionCreate returned no transaction and no error details.", + }, + ], + }; + } + + return { + transaction: transactionCreatePayload.transaction, + errors: [], + }; + }), + ); + + const failedTransactionCreates: FailedTransactionCreate[] = []; + let createdTransactionsCount = 0; + + settled.forEach((entry, index) => { + const checkoutId = checkoutIds[index]; + + if (!checkoutId) { + return; + } + + if (entry.status === "rejected") { + failedTransactionCreates.push({ + checkoutId, + errors: [ + { + code: "REQUEST_FAILED", + message: + entry.reason instanceof Error + ? entry.reason.message + : "transactionCreate failed.", + }, + ], + }); + + return; + } + + if (entry.value.errors.length > 0 || !entry.value.transaction) { + failedTransactionCreates.push({ + checkoutId, + errors: entry.value.errors.map((error) => ({ + code: error.code, + message: error.message ?? "Unknown transactionCreate error.", + })), + }); + + return; + } + + createdTransactionsCount += 1; + }); + + const hasAtLeastOneCreated = createdTransactionsCount > 0; + const hasFailures = failedTransactionCreates.length > 0; + + if (hasFailures) { + const errorResponse = { + error: "Failed to create transaction for one or more checkouts.", + paymentIntentId, + failedTransactionCreates, + }; + + if (hasAtLeastOneCreated) { + return NextResponse.json(errorResponse, { status: 200 }); + } + + return NextResponse.json(errorResponse, { status: 500 }); + } + + return NextResponse.json({ status: "processed", paymentIntentId }); +} diff --git a/apps/marketplace/src/graphql/generated/client.ts b/apps/marketplace/src/graphql/generated/client.ts index 9cd88fd0..3fe06d65 100644 --- a/apps/marketplace/src/graphql/generated/client.ts +++ b/apps/marketplace/src/graphql/generated/client.ts @@ -19418,7 +19418,10 @@ export type Payment = Node & ObjectWithMetadata & { modified: Scalars['DateTime']['output']; /** Order associated with a payment. */ order: Maybe; - /** Informs whether this is a partial payment. */ + /** + * Informs whether this is a partial payment. + * @deprecated This field is reserved for the Adyen Gateway plugin. For other gateways, its value is always `false`. This field will be removed in 3.23 along with the plugin. + */ partial: Scalars['Boolean']['output']; /** Type of method used for payment. */ paymentMethodType: Scalars['String']['output']; @@ -32734,6 +32737,24 @@ export type ProductVariantUpdateMutationVariables = Exact<{ export type ProductVariantUpdateMutation = ProductVariantUpdateMutation_Mutation; +export type TransactionCreate_transactionCreate_TransactionCreate_transaction_TransactionItem = { id: string, name: string }; + +export type TransactionCreate_transactionCreate_TransactionCreate_errors_TransactionCreateError = { field: string | null, message: string | null, code: TransactionCreateErrorCode }; + +export type TransactionCreate_transactionCreate_TransactionCreate = { transaction: TransactionCreate_transactionCreate_TransactionCreate_transaction_TransactionItem | null, errors: Array }; + +export type TransactionCreate_Mutation = { transactionCreate: TransactionCreate_transactionCreate_TransactionCreate | null }; + + +export type TransactionCreateVariables = Exact<{ + id: Scalars['ID']['input']; + transaction: TransactionCreateInput; + transactionEvent?: InputMaybe; +}>; + + +export type TransactionCreate = TransactionCreate_Mutation; + export type VendorCollectionCreate_collectionCreate_CollectionCreate_collection_Collection = { id: string, name: string, slug: string }; export type VendorCollectionCreate_collectionCreate_CollectionCreate_errors_CollectionError = { field: string | null, message: string | null, code: CollectionErrorCode }; @@ -32853,6 +32874,22 @@ export type ChannelsVariables = Exact<{ [key: string]: never; }>; export type Channels = Channels_Query; +export type CheckoutTransactions_checkout_Checkout_transactions_TransactionItem_chargedAmount_Money = { amount: number }; + +export type CheckoutTransactions_checkout_Checkout_transactions_TransactionItem = { id: string, pspReference: string, chargedAmount: CheckoutTransactions_checkout_Checkout_transactions_TransactionItem_chargedAmount_Money }; + +export type CheckoutTransactions_checkout_Checkout = { id: string, transactions: Array | null }; + +export type CheckoutTransactions_Query = { checkout: CheckoutTransactions_checkout_Checkout | null }; + + +export type CheckoutTransactionsVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type CheckoutTransactions = CheckoutTransactions_Query; + export type CollectionDetail_collection_Collection_backgroundImage_Image = { url: string, alt: string | null }; export type CollectionDetail_collection_Collection_metadata_MetadataItem = { key: string, value: string }; @@ -33239,19 +33276,7 @@ export type ProductDetail_product_Product_assignedAttributes_AssignedTextAttribu & { __typename: 'AssignedTextAttribute' } ); -export type ProductDetail_product_Product_assignedAttributes = - | ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute - | ProductDetail_product_Product_assignedAttributes_AssignedDateAttribute - | ProductDetail_product_Product_assignedAttributes_AssignedDateTimeAttribute - | ProductDetail_product_Product_assignedAttributes_AssignedFileAttribute - | ProductDetail_product_Product_assignedAttributes_EP9HcmmFCQqdoG7811YxTX6aNOeQ5zZmEGxiLqdpAO4 - | ProductDetail_product_Product_assignedAttributes_AssignedMultiChoiceAttribute - | ProductDetail_product_Product_assignedAttributes_AssignedNumericAttribute - | ProductDetail_product_Product_assignedAttributes_AssignedPlainTextAttribute - | ProductDetail_product_Product_assignedAttributes_AssignedSingleChoiceAttribute - | ProductDetail_product_Product_assignedAttributes_AssignedSwatchAttribute - | ProductDetail_product_Product_assignedAttributes_AssignedTextAttribute -; +export type ProductDetail_product_Product_assignedAttributes = ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute | ProductDetail_product_Product_assignedAttributes_AssignedDateAttribute | ProductDetail_product_Product_assignedAttributes_AssignedDateTimeAttribute | ProductDetail_product_Product_assignedAttributes_AssignedFileAttribute | ProductDetail_product_Product_assignedAttributes_EP9HcmmFCQqdoG7811YxTX6aNOeQ5zZmEGxiLqdpAO4 | ProductDetail_product_Product_assignedAttributes_AssignedMultiChoiceAttribute | ProductDetail_product_Product_assignedAttributes_AssignedNumericAttribute | ProductDetail_product_Product_assignedAttributes_AssignedPlainTextAttribute | ProductDetail_product_Product_assignedAttributes_AssignedSingleChoiceAttribute | ProductDetail_product_Product_assignedAttributes_AssignedSwatchAttribute | ProductDetail_product_Product_assignedAttributes_AssignedTextAttribute; export type ProductDetail_product_Product_pricing_ProductPricingInfo_priceRange_TaxedMoneyRange_start_TaxedMoney_gross_Money = { amount: number, currency: string }; @@ -33453,19 +33478,7 @@ export type ProductVariantDetail_productVariant_ProductVariant_assignedAttribute & { __typename: 'AssignedTextAttribute' } ); -export type ProductVariantDetail_productVariant_ProductVariant_assignedAttributes = - | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute - | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedDateAttribute - | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedDateTimeAttribute - | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedFileAttribute - | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_EP9HcmmFCQqdoG7811YxTX6aNOeQ5zZmEGxiLqdpAO4 - | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedMultiChoiceAttribute - | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedNumericAttribute - | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedPlainTextAttribute - | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedSingleChoiceAttribute - | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedSwatchAttribute - | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedTextAttribute -; +export type ProductVariantDetail_productVariant_ProductVariant_assignedAttributes = ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedDateAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedDateTimeAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedFileAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_EP9HcmmFCQqdoG7811YxTX6aNOeQ5zZmEGxiLqdpAO4 | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedMultiChoiceAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedNumericAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedPlainTextAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedSingleChoiceAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedSwatchAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedTextAttribute; export type ProductVariantDetail_productVariant_ProductVariant_stocks_Stock_warehouse_Warehouse = { id: string, name: string }; @@ -34402,6 +34415,25 @@ export const ProductVariantUpdateMutationDocument = new TypedDocumentString(` } } `) as unknown as TypedDocumentString; +export const TransactionCreateDocument = new TypedDocumentString(` + mutation TransactionCreate($id: ID!, $transaction: TransactionCreateInput!, $transactionEvent: TransactionEventInput) { + transactionCreate( + id: $id + transaction: $transaction + transactionEvent: $transactionEvent + ) { + transaction { + id + name + } + errors { + field + message + code + } + } +} + `) as unknown as TypedDocumentString; export const VendorCollectionCreateDocument = new TypedDocumentString(` mutation VendorCollectionCreate($input: CollectionCreateInput!) { collectionCreate(input: $input) { @@ -34526,6 +34558,20 @@ export const ChannelsDocument = new TypedDocumentString(` } } `) as unknown as TypedDocumentString; +export const CheckoutTransactionsDocument = new TypedDocumentString(` + query CheckoutTransactions($id: ID!) { + checkout(id: $id) { + id + transactions { + id + pspReference + chargedAmount { + amount + } + } + } +} + `) as unknown as TypedDocumentString; export const CollectionDetailDocument = new TypedDocumentString(` query CollectionDetail($id: ID!) { collection(id: $id) { diff --git a/apps/marketplace/src/graphql/mutations/TransactionCreate.graphql b/apps/marketplace/src/graphql/mutations/TransactionCreate.graphql new file mode 100644 index 00000000..07e2e7fc --- /dev/null +++ b/apps/marketplace/src/graphql/mutations/TransactionCreate.graphql @@ -0,0 +1,21 @@ +mutation TransactionCreate( + $id: ID! + $transaction: TransactionCreateInput! + $transactionEvent: TransactionEventInput +) { + transactionCreate( + id: $id + transaction: $transaction + transactionEvent: $transactionEvent + ) { + transaction { + id + name + } + errors { + field + message + code + } + } +} diff --git a/apps/marketplace/src/graphql/queries/CheckoutTransactions.graphql b/apps/marketplace/src/graphql/queries/CheckoutTransactions.graphql new file mode 100644 index 00000000..07918a96 --- /dev/null +++ b/apps/marketplace/src/graphql/queries/CheckoutTransactions.graphql @@ -0,0 +1,12 @@ +query CheckoutTransactions($id: ID!) { + checkout(id: $id) { + id + transactions { + id + pspReference + chargedAmount { + amount + } + } + } +} diff --git a/apps/marketplace/src/lib/graphql/server/auth.ts b/apps/marketplace/src/lib/graphql/server/auth.ts index 53853cd8..f61df444 100644 --- a/apps/marketplace/src/lib/graphql/server/auth.ts +++ b/apps/marketplace/src/lib/graphql/server/auth.ts @@ -185,6 +185,8 @@ const OPERATION_AUTH_CONFIG: Record = { customers: GraphQLAuthLevel.APP_TOKEN_ONLY, vendorProfiles: GraphQLAuthLevel.APP_TOKEN_ONLY, VendorProfilesQuery: GraphQLAuthLevel.APP_TOKEN_ONLY, + checkout: GraphQLAuthLevel.APP_TOKEN_ONLY, + transactionCreate: GraphQLAuthLevel.APP_TOKEN_ONLY, // Domain-only operations (require x-saleor-domain header but no auth token) accountRegister: GraphQLAuthLevel.DOMAIN_ONLY, diff --git a/apps/marketplace/src/lib/graphql/server/schema.ts b/apps/marketplace/src/lib/graphql/server/schema.ts index 3704cea1..ef30143d 100644 --- a/apps/marketplace/src/lib/graphql/server/schema.ts +++ b/apps/marketplace/src/lib/graphql/server/schema.ts @@ -115,6 +115,7 @@ async function makeSaleorSchema() { "categories", "channel", "channels", + "checkout", "collection", "collections", "customers", @@ -184,6 +185,7 @@ async function makeSaleorSchema() { "collectionDelete", "collectionAddProducts", "collectionRemoveProducts", + "transactionCreate", ]; return allowedMutations.includes(fieldName); diff --git a/apps/marketplace/src/lib/stripe/client.ts b/apps/marketplace/src/lib/stripe/client.ts new file mode 100644 index 00000000..b7247339 --- /dev/null +++ b/apps/marketplace/src/lib/stripe/client.ts @@ -0,0 +1,168 @@ +import { APP_CONFIG } from "@/lib/saleor/consts"; + +interface PaymentIntentCreateInput { + amount: number; + automatic_payment_methods: { + enabled: boolean; + }; + currency: string; + idempotencyKey?: string; + metadata?: Record; + transfer_group?: string; +} + +interface PaymentIntentCreateOutput { + client_secret: string | null; + id: string; +} + +interface RefundCreateInput { + amount: number; + idempotencyKey?: string; + metadata?: Record; + payment_intent: string; +} + +type RefundStatus = + | "pending" + | "requires_action" + | "succeeded" + | "failed" + | "canceled"; + +interface RefundCreateOutput { + amount: number; + currency: string; + failure_reason: string | null; + id: string; + status: RefundStatus; +} + +const toPaymentIntentFormBody = ( + input: PaymentIntentCreateInput, +): URLSearchParams => { + const form = new URLSearchParams(); + + form.set("amount", String(input.amount)); + form.set("currency", input.currency); + form.set( + "automatic_payment_methods[enabled]", + String(input.automatic_payment_methods.enabled), + ); + + if (input.transfer_group) { + form.set("transfer_group", input.transfer_group); + } + + Object.entries(input.metadata ?? {}).forEach(([key, value]) => { + form.set(`metadata[${key}]`, value); + }); + + return form; +}; + +const toRefundFormBody = (input: RefundCreateInput): URLSearchParams => { + const form = new URLSearchParams(); + + form.set("payment_intent", input.payment_intent); + form.set("amount", String(input.amount)); + + Object.entries(input.metadata ?? {}).forEach(([key, value]) => { + form.set(`metadata[${key}]`, value); + }); + + return form; +}; + +const parseStripeError = (data: unknown, fallbackMessage: string) => { + if (typeof data !== "object" || data === null || !("error" in data)) { + return fallbackMessage; + } + + const stripeError = data.error; + + if ( + typeof stripeError === "object" && + stripeError !== null && + "message" in stripeError && + typeof stripeError.message === "string" + ) { + return stripeError.message; + } + + return fallbackMessage; +}; + +const paymentIntents = { + create: async ( + input: PaymentIntentCreateInput, + ): Promise => { + const response = await fetch("https://api.stripe.com/v1/payment_intents", { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`, + "Content-Type": "application/x-www-form-urlencoded", + ...(input.idempotencyKey + ? { "Idempotency-Key": input.idempotencyKey } + : {}), + }, + body: toPaymentIntentFormBody(input).toString(), + }); + + const data = (await response.json()) as unknown; + + if (!response.ok) { + throw new Error( + parseStripeError(data, "Stripe payment intent create failed."), + ); + } + + if ( + typeof data !== "object" || + data === null || + !("id" in data) || + !("client_secret" in data) + ) { + throw new Error("Invalid Stripe response."); + } + + return data as PaymentIntentCreateOutput; + }, +}; + +const refunds = { + create: async (input: RefundCreateInput): Promise => { + const response = await fetch("https://api.stripe.com/v1/refunds", { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`, + "Content-Type": "application/x-www-form-urlencoded", + ...(input.idempotencyKey + ? { "Idempotency-Key": input.idempotencyKey } + : {}), + }, + body: toRefundFormBody(input).toString(), + }); + + const data = (await response.json()) as unknown; + + if (!response.ok) { + throw new Error(parseStripeError(data, "Stripe refund create failed.")); + } + + if ( + typeof data !== "object" || + data === null || + !("id" in data) || + !("status" in data) || + !("amount" in data) || + !("currency" in data) + ) { + throw new Error("Invalid Stripe refund response."); + } + + return data as RefundCreateOutput; + }, +}; + +export const getStripeClient = () => ({ paymentIntents, refunds }); diff --git a/apps/marketplace/src/lib/stripe/currency.ts b/apps/marketplace/src/lib/stripe/currency.ts new file mode 100644 index 00000000..f96a8b61 --- /dev/null +++ b/apps/marketplace/src/lib/stripe/currency.ts @@ -0,0 +1,51 @@ +type Money = { + amount: number; + currency: string; +}; + +export const getDecimalsForStripe = (currency: string) => { + if (currency.length !== 3) { + throw new Error(`Currency ${currency} should be 3 characters long.`); + } + + const map = { + BIF: 0, + CLP: 0, + DJF: 0, + GNF: 0, + JPY: 0, + KMF: 0, + KRW: 0, + MGA: 0, + PYG: 0, + RWF: 0, + UGX: 0, + VND: 0, + VUV: 0, + XAF: 0, + XOF: 0, + XPF: 0, + BHD: 3, + JOD: 3, + KWD: 3, + OMR: 3, + TND: 3, + }; + + return map[currency as keyof typeof map] ?? 2; +}; + +export const getCentsFromAmount = (money: Money) => { + const amount = Number(money.amount); + const decimals = getDecimalsForStripe(money.currency); + const multiplier = 10 ** decimals; + + return Math.round(amount * multiplier); +}; + +export const getAmountFromCents = (money: Money) => { + const decimals = getDecimalsForStripe(money.currency); + const multiplier = 10 ** decimals; + + return (money.amount / multiplier).toFixed(decimals); +}; diff --git a/apps/marketplace/src/lib/stripe/webhook-signature.ts b/apps/marketplace/src/lib/stripe/webhook-signature.ts new file mode 100644 index 00000000..40444dd8 --- /dev/null +++ b/apps/marketplace/src/lib/stripe/webhook-signature.ts @@ -0,0 +1,71 @@ +import { createHmac, timingSafeEqual } from "crypto"; + +const WEBHOOK_TOLERANCE_SECONDS = 300; + +const parseStripeSignature = (signature: string) => { + const parts = signature.split(",").map((part) => part.trim()); + const timestamp = parts.find((part) => part.startsWith("t="))?.slice(2); + const signatures = parts + .filter((part) => part.startsWith("v1=")) + .map((part) => part.slice(3)); + + return { + timestamp, + signatures, + }; +}; + +export const verifyStripeWebhookSignature = ({ + payload, + stripeSignature, +}: { + payload: string; + stripeSignature: string; +}) => { + const { timestamp, signatures } = parseStripeSignature(stripeSignature); + + if (!timestamp || signatures.length === 0) { + return false; + } + + const timestampNumber = Number(timestamp); + + if (!Number.isFinite(timestampNumber)) { + return false; + } + + const currentTimestamp = Math.floor(Date.now() / 1000); + + if ( + Math.abs(currentTimestamp - timestampNumber) > WEBHOOK_TOLERANCE_SECONDS + ) { + return false; + } + + const signedPayload = `${timestamp}.${payload}`; + const secret = process.env.STRIPE_WEBHOOK_SIGNING_SECRET; + + if (!secret) { + throw new Error("Missing STRIPE_WEBHOOK_SIGNING_SECRET"); + } + + const expectedSignature = createHmac("sha256", secret) + .update(signedPayload, "utf8") + .digest("hex"); + + const expectedBuffer = Buffer.from(expectedSignature, "hex"); + + return signatures.some((signature) => { + try { + const signatureBuffer = Buffer.from(signature, "hex"); + + if (signatureBuffer.length !== expectedBuffer.length) { + return false; + } + + return timingSafeEqual(signatureBuffer, expectedBuffer); + } catch { + return false; + } + }); +}; diff --git a/apps/marketplace/src/services/transactions.ts b/apps/marketplace/src/services/transactions.ts new file mode 100644 index 00000000..e7531416 --- /dev/null +++ b/apps/marketplace/src/services/transactions.ts @@ -0,0 +1,35 @@ +import { + CheckoutTransactionsDocument, + type CheckoutTransactionsVariables, + TransactionCreateDocument, + type TransactionCreateVariables, +} from "@/graphql/generated/client"; +import { executeGraphQL } from "@/lib/graphql/execute"; + +class TransactionsService { + async getCheckoutTransactions( + variables: CheckoutTransactionsVariables, + token?: string | null, + ) { + return executeGraphQL( + CheckoutTransactionsDocument, + "CheckoutTransactionsQuery", + variables, + token, + ); + } + + async createTransaction( + variables?: TransactionCreateVariables, + token?: string | null, + ) { + return executeGraphQL( + TransactionCreateDocument, + "CreateTransactionMutation", + variables, + token, + ); + } +} + +export const transactionsService = new TransactionsService(); From bfcd077b52f85270a9903131f735dd2239b1b3c5 Mon Sep 17 00:00:00 2001 From: Tomasz Stuba Date: Sun, 22 Mar 2026 19:57:05 +0100 Subject: [PATCH 06/23] feat: adding checkoutComplete to Stripe webhook --- .../app/api/payments/stripe/webhooks/route.ts | 142 +++++++++++++++++- .../src/graphql/generated/client.ts | 30 ++++ .../mutations/CheckoutComplete.graphql | 12 ++ .../src/lib/graphql/server/auth.ts | 1 + .../src/lib/graphql/server/schema.ts | 1 + apps/marketplace/src/services/checkouts.ts | 21 +++ apps/marketplace/src/services/index.ts | 2 + 7 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 apps/marketplace/src/graphql/mutations/CheckoutComplete.graphql create mode 100644 apps/marketplace/src/services/checkouts.ts diff --git a/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts b/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts index d3ab5a05..b2069bb5 100644 --- a/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts +++ b/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts @@ -3,6 +3,8 @@ import { type NextRequest, NextResponse } from "next/server"; import type { TransactionCreateVariables } from "@/graphql/generated/client"; import { getAppConfig } from "@/lib/saleor/app-config"; import { verifyStripeWebhookSignature } from "@/lib/stripe/webhook-signature"; +import { checkoutService } from "@/services/checkouts"; +import { marketplaceLogger } from "@/services/logging"; import { transactionsService } from "@/services/transactions"; type StripePaymentIntentSucceededEvent = { @@ -26,6 +28,11 @@ type FailedTransactionCreate = { errors: Array<{ code: string; message: string }>; }; +type ServiceError = { + code: string; + message: string; +}; + type TransactionCreatePayload = { errors: Array<{ code: string; message: string | null }>; transaction: { id: string; name: string } | null; @@ -40,6 +47,35 @@ type CheckoutTransactionsPayload = { } | null; }; +type CheckoutCompletePayload = { + checkoutComplete: { + errors: Array<{ code: string; message: string | null }>; + order: { id: string } | null; + } | null; +}; + +const isCheckoutCompleteNotFoundError = (code: string) => code === "NOT_FOUND"; + +const mapCheckoutCompleteErrors = ( + errors: Array<{ code: string; message?: string | null }>, +): ServiceError[] => + errors + .filter((error) => !isCheckoutCompleteNotFoundError(error.code)) + .map((error) => ({ + code: error.code, + message: error.message ?? "checkoutComplete failed.", + })); + +const mapCheckoutCompleteRequestFailure = (error: unknown): ServiceError[] => [ + { + code: "CHECKOUT_COMPLETE_REQUEST_FAILED", + message: + error instanceof Error + ? error.message + : "checkoutComplete request failed.", + }, +]; + const getSaleorDomainFromEnv = () => { const saleorUrl = process.env.NEXT_PUBLIC_SALEOR_URL; @@ -181,6 +217,70 @@ export async function POST(request: NextRequest) { ); } + const completeCheckout = async ( + checkoutId: string, + ): Promise => { + const checkoutCompleteResult = await checkoutService.completeCheckout( + { id: checkoutId }, + config.authToken, + ); + + if (!checkoutCompleteResult.ok) { + const mappedCheckoutCompleteErrors = mapCheckoutCompleteErrors( + checkoutCompleteResult.errors, + ); + + if ( + mappedCheckoutCompleteErrors.length === 0 && + checkoutCompleteResult.errors.length > 0 + ) { + return []; + } + + if (mappedCheckoutCompleteErrors.length === 0) { + return [ + { + code: "UNKNOWN_CHECKOUT_COMPLETE_ERROR", + message: "checkoutComplete failed without error details.", + }, + ]; + } + + return mappedCheckoutCompleteErrors; + } + + const checkoutCompleteResultData = + checkoutCompleteResult.data as CheckoutCompletePayload; + const rawCheckoutCompleteErrors = + checkoutCompleteResultData.checkoutComplete?.errors ?? []; + const mappedCheckoutCompleteErrors = mapCheckoutCompleteErrors( + rawCheckoutCompleteErrors, + ); + const hasOnlyNotFoundErrors = + rawCheckoutCompleteErrors.length > 0 && + rawCheckoutCompleteErrors.every((error) => + isCheckoutCompleteNotFoundError(error.code), + ); + + if (mappedCheckoutCompleteErrors.length > 0) { + return mappedCheckoutCompleteErrors; + } + + if ( + checkoutCompleteResultData.checkoutComplete?.order || + hasOnlyNotFoundErrors + ) { + return []; + } + + return [ + { + code: "UNKNOWN_CHECKOUT_COMPLETE_ERROR", + message: "checkoutComplete returned no order and no error details.", + }, + ]; + }; + const settled = await Promise.allSettled( checkoutIds.map(async (checkoutId) => { const amount = checkoutAmounts[checkoutId]; @@ -212,6 +312,31 @@ export async function POST(request: NextRequest) { }) ?? false; if (alreadyCharged) { + marketplaceLogger.warning( + "Stripe webhook recovery path: checkout already charged, retrying checkoutComplete.", + { + checkoutId, + eventId: event.id, + paymentIntentId, + saleorDomain, + }, + ); + + let checkoutCompleteErrors: ServiceError[]; + + try { + checkoutCompleteErrors = await completeCheckout(checkoutId); + } catch (error) { + checkoutCompleteErrors = mapCheckoutCompleteRequestFailure(error); + } + + if (checkoutCompleteErrors.length > 0) { + return { + transaction: null, + errors: checkoutCompleteErrors, + }; + } + return { transaction: { id: "already-processed", @@ -242,8 +367,6 @@ export async function POST(request: NextRequest) { config.authToken, ); - // checkotu complete here - if (!transactionCreateResult.ok) { return { transaction: null, @@ -286,6 +409,21 @@ export async function POST(request: NextRequest) { }; } + let checkoutCompleteErrors: ServiceError[]; + + try { + checkoutCompleteErrors = await completeCheckout(checkoutId); + } catch (error) { + checkoutCompleteErrors = mapCheckoutCompleteRequestFailure(error); + } + + if (checkoutCompleteErrors.length > 0) { + return { + transaction: null, + errors: checkoutCompleteErrors, + }; + } + return { transaction: transactionCreatePayload.transaction, errors: [], diff --git a/apps/marketplace/src/graphql/generated/client.ts b/apps/marketplace/src/graphql/generated/client.ts index 3fe06d65..82aee36c 100644 --- a/apps/marketplace/src/graphql/generated/client.ts +++ b/apps/marketplace/src/graphql/generated/client.ts @@ -32074,6 +32074,22 @@ export type AccountUpdateMutationVariables = Exact<{ export type AccountUpdateMutation = AccountUpdateMutation_Mutation; +export type CheckoutCompleteMutation_checkoutComplete_CheckoutComplete_order_Order = { id: string }; + +export type CheckoutCompleteMutation_checkoutComplete_CheckoutComplete_errors_CheckoutError = { field: string | null, message: string | null, code: CheckoutErrorCode }; + +export type CheckoutCompleteMutation_checkoutComplete_CheckoutComplete = { order: CheckoutCompleteMutation_checkoutComplete_CheckoutComplete_order_Order | null, errors: Array }; + +export type CheckoutCompleteMutation_Mutation = { checkoutComplete: CheckoutCompleteMutation_checkoutComplete_CheckoutComplete | null }; + + +export type CheckoutCompleteMutationVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type CheckoutCompleteMutation = CheckoutCompleteMutation_Mutation; + export type CollectionAddProductsMutation_collectionAddProducts_CollectionAddProducts_errors_CollectionError = { field: string | null, message: string | null, code: CollectionErrorCode }; export type CollectionAddProductsMutation_collectionAddProducts_CollectionAddProducts_collection_Collection = { id: string, name: string }; @@ -33709,6 +33725,20 @@ export const AccountUpdateMutationDocument = new TypedDocumentString(` } } `) as unknown as TypedDocumentString; +export const CheckoutCompleteMutationDocument = new TypedDocumentString(` + mutation CheckoutCompleteMutation($id: ID!) { + checkoutComplete(id: $id) { + order { + id + } + errors { + field + message + code + } + } +} + `) as unknown as TypedDocumentString; export const CollectionAddProductsMutationDocument = new TypedDocumentString(` mutation CollectionAddProductsMutation($collectionId: ID!, $products: [ID!]!) { collectionAddProducts(collectionId: $collectionId, products: $products) { diff --git a/apps/marketplace/src/graphql/mutations/CheckoutComplete.graphql b/apps/marketplace/src/graphql/mutations/CheckoutComplete.graphql new file mode 100644 index 00000000..b9c86b1d --- /dev/null +++ b/apps/marketplace/src/graphql/mutations/CheckoutComplete.graphql @@ -0,0 +1,12 @@ +mutation CheckoutCompleteMutation($id: ID!) { + checkoutComplete(id: $id) { + order { + id + } + errors { + field + message + code + } + } +} diff --git a/apps/marketplace/src/lib/graphql/server/auth.ts b/apps/marketplace/src/lib/graphql/server/auth.ts index f61df444..3ef0d377 100644 --- a/apps/marketplace/src/lib/graphql/server/auth.ts +++ b/apps/marketplace/src/lib/graphql/server/auth.ts @@ -186,6 +186,7 @@ const OPERATION_AUTH_CONFIG: Record = { vendorProfiles: GraphQLAuthLevel.APP_TOKEN_ONLY, VendorProfilesQuery: GraphQLAuthLevel.APP_TOKEN_ONLY, checkout: GraphQLAuthLevel.APP_TOKEN_ONLY, + checkoutComplete: GraphQLAuthLevel.APP_TOKEN_ONLY, transactionCreate: GraphQLAuthLevel.APP_TOKEN_ONLY, // Domain-only operations (require x-saleor-domain header but no auth token) diff --git a/apps/marketplace/src/lib/graphql/server/schema.ts b/apps/marketplace/src/lib/graphql/server/schema.ts index ef30143d..fd8a24ae 100644 --- a/apps/marketplace/src/lib/graphql/server/schema.ts +++ b/apps/marketplace/src/lib/graphql/server/schema.ts @@ -185,6 +185,7 @@ async function makeSaleorSchema() { "collectionDelete", "collectionAddProducts", "collectionRemoveProducts", + "checkoutComplete", "transactionCreate", ]; diff --git a/apps/marketplace/src/services/checkouts.ts b/apps/marketplace/src/services/checkouts.ts new file mode 100644 index 00000000..ef317ca3 --- /dev/null +++ b/apps/marketplace/src/services/checkouts.ts @@ -0,0 +1,21 @@ +import { + CheckoutCompleteMutationDocument, + type CheckoutCompleteMutationVariables, +} from "@/graphql/generated/client"; +import { executeGraphQL } from "@/lib/graphql/execute"; + +class CheckoutService { + async completeCheckout( + variables: CheckoutCompleteMutationVariables, + token?: string | null, + ) { + return executeGraphQL( + CheckoutCompleteMutationDocument, + "CheckoutCompleteMutation", + variables, + token, + ); + } +} + +export const checkoutService = new CheckoutService(); diff --git a/apps/marketplace/src/services/index.ts b/apps/marketplace/src/services/index.ts index e2efe3dd..e7341f77 100644 --- a/apps/marketplace/src/services/index.ts +++ b/apps/marketplace/src/services/index.ts @@ -1,6 +1,8 @@ +export { checkoutService } from "./checkouts"; export { collectionsService } from "./collections"; export { configurationService } from "./configuration"; export { ordersService } from "./orders"; export { productsService } from "./products"; +export { transactionsService } from "./transactions"; export { vendorsService } from "./vendors"; export { vendorCustomersService } from "./vendor-customers"; From ca3437835f57bd7b2206572d7aa4408d3b765a0a Mon Sep 17 00:00:00 2001 From: Tomasz Stuba Date: Mon, 23 Mar 2026 14:30:08 +0100 Subject: [PATCH 07/23] feat: add permission in manifest --- apps/marketplace/src/app/api/saleor/manifest/route.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/marketplace/src/app/api/saleor/manifest/route.ts b/apps/marketplace/src/app/api/saleor/manifest/route.ts index a10b2d9c..7ddf1f31 100644 --- a/apps/marketplace/src/app/api/saleor/manifest/route.ts +++ b/apps/marketplace/src/app/api/saleor/manifest/route.ts @@ -57,6 +57,7 @@ export async function GET(request: NextRequest) { "MANAGE_ORDERS", "MANAGE_SHIPPING", "MANAGE_CHANNELS", + "HANDLE_PAYMENTS", ], tokenTargetUrl: manifestUrl(baseUrl, "/api/saleor/register"), appUrl: manifestUrl(baseUrl, "/app"), From b6c52566b375f8264fa757b5a01bc3f9d49468ff Mon Sep 17 00:00:00 2001 From: Tomasz Stuba Date: Thu, 26 Mar 2026 11:39:25 +0100 Subject: [PATCH 08/23] feat: payments for marketplace integrated with storefront --- .../app/api/payments/payment-intent/route.ts | 282 +++++++++++++++--- .../app/[locale]/(checkout)/checkout/page.tsx | 114 ++++++- .../_components/marketplace-cart-view.tsx | 90 ++++++ .../src/app/[locale]/(main)/cart/page.tsx | 24 ++ .../(main)/payment/confirmation/page.tsx | 31 ++ .../products/[slug]/_actions/add-to-bag.ts | 24 +- .../src/features/checkout/aggregations.ts | 148 +++++++++ apps/storefront/src/features/checkout/cart.ts | 218 +++++++++++++- .../src/features/checkout/checkout-actions.ts | 112 ++++++- .../src/features/checkout/constants.ts | 1 + .../src/features/checkout/summary.tsx | 14 +- .../storefront/src/features/checkout/types.ts | 8 + .../storefront/src/features/header/header.tsx | 48 ++- apps/storefront/src/foundation/auth/login.ts | 28 +- .../actions/update-checkout-address-action.ts | 27 +- .../sections/delivery-method/actions.ts | 41 +++ .../delivery-method/marketplace-form.tsx | 138 +++++++++ .../checkout/sections/payment/actions.ts | 86 +++++- .../checkout/sections/payment/payment.tsx | 282 ++++++++++++++---- .../foundation/checkout/sections/sections.tsx | 104 +++++-- .../checkout/sections/user-details/actions.ts | 38 ++- .../cart/shared/components/cart-details.tsx | 14 +- .../shared/actions/add-to-bag.core.ts | 44 +-- .../shared/components/add-to-bag.tsx | 105 +------ .../shared/components/variant-selector.tsx | 1 - .../infrastructure/payment-execute-infra.ts | 13 +- 26 files changed, 1722 insertions(+), 313 deletions(-) create mode 100644 apps/storefront/src/app/[locale]/(main)/cart/_components/marketplace-cart-view.tsx create mode 100644 apps/storefront/src/features/checkout/aggregations.ts create mode 100644 apps/storefront/src/features/checkout/constants.ts create mode 100644 apps/storefront/src/features/checkout/types.ts create mode 100644 apps/storefront/src/foundation/checkout/sections/delivery-method/marketplace-form.tsx diff --git a/apps/marketplace/src/app/api/payments/payment-intent/route.ts b/apps/marketplace/src/app/api/payments/payment-intent/route.ts index 8179ecd3..39e75a35 100644 --- a/apps/marketplace/src/app/api/payments/payment-intent/route.ts +++ b/apps/marketplace/src/app/api/payments/payment-intent/route.ts @@ -3,10 +3,11 @@ import { type NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import type { TransactionCreateVariables } from "@/graphql/generated/client"; -import { getAppConfig } from "@/lib/saleor/app-config"; import { getServerAuthToken } from "@/lib/auth/server"; -import { getCentsFromAmount } from "@/lib/stripe/currency"; +import { getAppConfig } from "@/lib/saleor/app-config"; import { getStripeClient } from "@/lib/stripe/client"; +import { getCentsFromAmount } from "@/lib/stripe/currency"; +import { marketplaceLogger } from "@/services/logging"; import { transactionsService } from "@/services/transactions"; const bodySchema = z @@ -37,8 +38,8 @@ const bodySchema = z }); type FailedTransactionCreate = { - errors: Array<{ code: string; message: string }>; checkoutId: string; + errors: Array<{ code: string; message: string }>; }; type TransactionCreatePayload = { @@ -46,8 +47,22 @@ type TransactionCreatePayload = { transaction: { id: string; name: string } | null; }; +type CheckoutTransactionsPayload = { + checkout: { + transactions: Array<{ + pspReference: string | null; + }> | null; + } | null; +}; + +type CheckoutInitializationStatus = { + checkoutId: string; + errors: Array<{ code: string; message: string }>; + status: "created" | "failed" | "skipped_existing"; +}; + const buildIdempotencyKey = ( - checkouts: { amount: number; currency: string; checkoutId: string }[], + checkouts: { amount: number; checkoutId: string; currency: string }[], ) => { const canonical = [...checkouts] .sort((a, b) => a.checkoutId.localeCompare(b.checkoutId)) @@ -108,7 +123,16 @@ export async function POST(request: NextRequest) { ); } - const currency = currencies[0]!; + const currency = currencies[0]; + + if (!currency) { + return NextResponse.json( + { + error: "Unable to determine checkout currency.", + }, + { status: 422 }, + ); + } const amount = checkouts.reduce( (sum, item) => sum + @@ -137,10 +161,126 @@ export async function POST(request: NextRequest) { marketplace_model: "separate_charges_transfers", }, }); + + if (!paymentIntent.client_secret) { + marketplaceLogger.error("PaymentIntent created without client secret", { + paymentIntentId: paymentIntent.id, + saleorDomain, + }); + + return NextResponse.json( + { + error: "Missing client secret for created PaymentIntent.", + paymentIntentId: paymentIntent.id, + }, + { status: 500 }, + ); + } + const token = await getServerAuthToken(); + const checkoutStatuses: CheckoutInitializationStatus[] = []; + const checkoutsToCreate: typeof checkouts = []; + + const checkoutTransactionsSettled = await Promise.allSettled( + checkouts.map((checkout) => + transactionsService.getCheckoutTransactions( + { id: checkout.checkoutId }, + token, + ), + ), + ); + + checkoutTransactionsSettled.forEach((entry, index) => { + const checkout = checkouts[index]; + + if (!checkout) { + return; + } + + const checkoutId = checkout.checkoutId; + + if (entry.status === "rejected") { + const message = + entry.reason instanceof Error + ? entry.reason.message + : "checkout transactions request failed."; + const errors = [{ code: "REQUEST_FAILED", message }]; + + marketplaceLogger.error( + "Failed to read checkout transactions before transactionCreate", + { + checkoutId, + paymentIntentId: paymentIntent.id, + saleorDomain, + errors, + }, + ); + + checkoutStatuses.push({ + checkoutId, + errors, + status: "failed", + }); + + return; + } + + if (!entry.value.ok) { + const errors = entry.value.errors.map((error) => ({ + code: error.code, + message: error.message ?? "checkout transactions request failed.", + })); + + marketplaceLogger.error( + "Checkout transactions query returned application errors", + { + checkoutId, + paymentIntentId: paymentIntent.id, + saleorDomain, + errors, + }, + ); + + checkoutStatuses.push({ + checkoutId, + errors, + status: "failed", + }); + + return; + } + + const checkoutTransactionsData = + entry.value.data as CheckoutTransactionsPayload; + const hasExistingTransaction = + checkoutTransactionsData.checkout?.transactions?.some( + (transaction) => transaction.pspReference === paymentIntent.id, + ) ?? false; + + if (hasExistingTransaction) { + marketplaceLogger.warning( + "Skipping transactionCreate for checkout because transaction already exists.", + { + checkoutId, + paymentIntentId: paymentIntent.id, + saleorDomain, + }, + ); + + checkoutStatuses.push({ + checkoutId, + errors: [], + status: "skipped_existing", + }); + + return; + } + + checkoutsToCreate.push(checkout); + }); const transactionCreateSettled = await Promise.allSettled( - checkouts.map((checkout) => { + checkoutsToCreate.map((checkout) => { const transactionVariables: TransactionCreateVariables = { id: checkout.checkoutId, transaction: { @@ -165,35 +305,63 @@ export async function POST(request: NextRequest) { ); const failedTransactionCreates: FailedTransactionCreate[] = []; - let createdTransactionsCount = 0; transactionCreateSettled.forEach((entry, index) => { - const checkoutId = checkouts[index]!.checkoutId; + const checkoutId = checkoutsToCreate[index]?.checkoutId; + + if (!checkoutId) { + return; + } if (entry.status === "rejected") { - failedTransactionCreates.push({ + const errors = [ + { + code: "REQUEST_FAILED", + message: + entry.reason instanceof Error + ? entry.reason.message + : "transactionCreate failed.", + }, + ]; + + marketplaceLogger.error("transactionCreate request failed", { checkoutId, - errors: [ - { - code: "REQUEST_FAILED", - message: - entry.reason instanceof Error - ? entry.reason.message - : "transactionCreate failed.", - }, - ], + paymentIntentId: paymentIntent.id, + saleorDomain, + errors, + }); + + failedTransactionCreates.push({ checkoutId, errors }); + checkoutStatuses.push({ + checkoutId, + errors, + status: "failed", }); return; } if (!entry.value.ok) { - failedTransactionCreates.push({ + const errors = entry.value.errors.map((error) => ({ + code: error.code, + message: error.message ?? "transactionCreate failed.", + })); + + marketplaceLogger.error( + "transactionCreate returned application errors", + { + checkoutId, + paymentIntentId: paymentIntent.id, + saleorDomain, + errors, + }, + ); + + failedTransactionCreates.push({ checkoutId, errors }); + checkoutStatuses.push({ checkoutId, - errors: entry.value.errors.map((error) => ({ - code: error.code, - message: error.message ?? "transactionCreate failed.", - })), + errors, + status: "failed", }); return; @@ -215,43 +383,58 @@ export async function POST(request: NextRequest) { code: error.code, message: error.message ?? "Unknown transactionCreate error.", })); + const errors = + mappedErrors && mappedErrors.length > 0 + ? mappedErrors + : [ + { + code: "UNKNOWN_TRANSACTION_CREATE_ERROR", + message: + "transactionCreate returned no transaction and no error details.", + }, + ]; + + marketplaceLogger.error("transactionCreate returned invalid payload", { + checkoutId, + paymentIntentId: paymentIntent.id, + saleorDomain, + errors, + }); - failedTransactionCreates.push({ + failedTransactionCreates.push({ checkoutId, errors }); + checkoutStatuses.push({ checkoutId, - errors: - mappedErrors && mappedErrors.length > 0 - ? mappedErrors - : [ - { - code: "UNKNOWN_TRANSACTION_CREATE_ERROR", - message: - "transactionCreate returned no transaction and no error details.", - }, - ], + errors, + status: "failed", }); return; } - createdTransactionsCount += 1; + marketplaceLogger.warning("transactionCreate succeeded for checkout", { + checkoutId, + paymentIntentId: paymentIntent.id, + saleorDomain, + }); + + checkoutStatuses.push({ + checkoutId, + errors: [], + status: "created", + }); }); - const hasAtLeastOneCreated = createdTransactionsCount > 0; const hasFailures = failedTransactionCreates.length > 0; if (hasFailures) { - const errorResponse = { - error: "Failed to create transaction for one or more orders.", - paymentIntentId: paymentIntent.id, - transferGroup, - failedTransactionCreates, - }; - - if (hasAtLeastOneCreated) { - return NextResponse.json(errorResponse, { status: 200 }); - } - - return NextResponse.json(errorResponse, { status: 500 }); + marketplaceLogger.error( + "Payment intent initialized with transactionCreate failures", + { + paymentIntentId: paymentIntent.id, + saleorDomain, + failedTransactionCreates, + }, + ); } return NextResponse.json( @@ -262,6 +445,9 @@ export async function POST(request: NextRequest) { currency: currency.toLowerCase(), amount, checkouts, + checkoutStatuses, + failedTransactionCreates, + hasFailures, }, { status: 200 }, ); diff --git a/apps/storefront/src/app/[locale]/(checkout)/checkout/page.tsx b/apps/storefront/src/app/[locale]/(checkout)/checkout/page.tsx index 827556f7..a1bdec4e 100644 --- a/apps/storefront/src/app/[locale]/(checkout)/checkout/page.tsx +++ b/apps/storefront/src/app/[locale]/(checkout)/checkout/page.tsx @@ -5,7 +5,11 @@ import { type AllCountryCode } from "@nimara/domain/consts"; import { type AppErrorCode } from "@nimara/domain/objects/Error"; import { redirect } from "@nimara/i18n/routing"; -import { getCheckoutOrRedirect } from "@/features/checkout/checkout-actions"; +import { + getCheckoutOrRedirect, + getMarketplaceCheckoutsOrRedirect, + getMarketplaceCheckoutSummary, +} from "@/features/checkout/checkout-actions"; import { Summary } from "@/features/checkout/summary"; import { CHECKOUT_STEPS_MAP, @@ -34,6 +38,17 @@ export const metadata: Metadata = { }; export default async function Page(props: PageProps) { + const isMarketplaceEnabled = + process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; + + if (isMarketplaceEnabled) { + return renderMarketplaceCheckoutPage(props); + } + + return renderLegacyCheckoutPage(props); +} + +const renderLegacyCheckoutPage = async (props: PageProps) => { const [{ locale }, searchParams, checkout, accessToken, services] = await Promise.all([ props.params, @@ -115,4 +130,99 @@ export default async function Page(props: PageProps) { /> ); -} +}; + +const renderMarketplaceCheckoutPage = async (props: PageProps) => { + const [{ locale }, searchParams, checkoutItems, accessToken, services] = + await Promise.all([ + props.params, + props.searchParams, + getMarketplaceCheckoutsOrRedirect(), + getAccessToken(), + getServiceRegistry(), + ]); + + const checkoutSummary = getMarketplaceCheckoutSummary(checkoutItems); + const primaryCheckout = + checkoutItems.find((item) => item.checkout.isShippingRequired)?.checkout ?? + checkoutItems[0].checkout; + + const currentStep = searchParams.step; + + if (!currentStep) { + let step: CheckoutStep | null = null; + + const requiresEmail = checkoutItems.some( + (item) => item.checkout.email === null, + ); + const requiresShipping = checkoutItems.some( + (item) => item.checkout.isShippingRequired, + ); + const requiresShippingAddress = checkoutItems.some( + (item) => + item.checkout.isShippingRequired && + item.checkout.shippingAddress === null, + ); + const requiresDeliveryMethod = checkoutItems.some( + (item) => + item.checkout.isShippingRequired && + item.checkout.deliveryMethod === null, + ); + + if (requiresEmail) { + step = CHECKOUT_STEPS_MAP.USER_DETAILS; + } else if (!requiresShipping) { + step = CHECKOUT_STEPS_MAP.PAYMENT; + } else if (requiresShippingAddress) { + step = CHECKOUT_STEPS_MAP.SHIPPING_ADDRESS; + } else if (requiresDeliveryMethod) { + step = CHECKOUT_STEPS_MAP.DELIVERY_METHOD; + } else { + step = CHECKOUT_STEPS_MAP.PAYMENT; + } + + redirect({ + href: paths.checkout.asPath({ query: { step } }), + locale, + }); + } + + const userService = await services.getUserService(); + const resultUserGet = await userService.userGet(accessToken); + const user = resultUserGet.ok ? resultUserGet.data : null; + const shippingAddressSectionData = checkoutSummary.isShippingRequired + ? await getCheckoutShippingAddressSectionData({ + accessToken, + checkout: primaryCheckout, + country: searchParams.country, + locale, + user, + }) + : null; + const paymentSectionData = + currentStep === CHECKOUT_STEPS_MAP.PAYMENT + ? await getCheckoutPaymentSectionData({ + accessToken, + checkout: primaryCheckout, + country: searchParams.country, + errorCode: searchParams.errorCode, + locale, + user, + }) + : null; + + return ( + } + > + + + ); +}; diff --git a/apps/storefront/src/app/[locale]/(main)/cart/_components/marketplace-cart-view.tsx b/apps/storefront/src/app/[locale]/(main)/cart/_components/marketplace-cart-view.tsx new file mode 100644 index 00000000..fc2fe39e --- /dev/null +++ b/apps/storefront/src/app/[locale]/(main)/cart/_components/marketplace-cart-view.tsx @@ -0,0 +1,90 @@ +import { type Cart } from "@nimara/domain/objects/Cart"; +import { CartDetails } from "@nimara/features/cart/shared/components/cart-details"; +import { EmptyCart } from "@nimara/features/cart/shared/components/empty-cart"; +import { type CartViewProps } from "@nimara/features/cart/shared/types"; + +import { aggregateCarts } from "@/features/checkout/aggregations"; +import { getCheckoutIdsByVendor } from "@/features/checkout/cart"; + +export const MarketplaceCartView = async (props: CartViewProps) => { + const { + services, + accessToken, + paths, + onCartUpdate, + onLineQuantityChange, + onLineDelete, + region, + logger, + } = props; + + const checkoutIdsByVendor = await getCheckoutIdsByVendor(); + const checkoutIds = [...new Set(Object.values(checkoutIdsByVendor))]; + + if (!checkoutIds.length) { + return ; + } + + const cartService = await services.getCartService(); + const cartResults = await Promise.all( + checkoutIds.map(async (checkoutId) => { + const result = await cartService.cartGet({ + cartId: checkoutId, + languageCode: region.language.code, + countryCode: region.market.countryCode, + options: { + next: { + revalidate: services.config.cacheTTL.cart, + tags: [`CHECKOUT:${checkoutId}`], + }, + }, + }); + + if (!result.ok || result.data.lines.length === 0) { + return null; + } + + return { + checkoutId, + cart: result.data, + }; + }), + ); + + const cartsWithIds = cartResults.filter( + (entry): entry is { cart: Cart; checkoutId: string } => entry !== null, + ); + + if (!cartsWithIds.length) { + logger.debug("No active marketplace checkouts. Rendering empty cart."); + + return ; + } + + const { aggregatedCart, lineCheckoutIdMap } = aggregateCarts(cartsWithIds); + + const userService = await services.getUserService(); + const resultUserGet = accessToken + ? await userService.userGet(accessToken) + : { ok: false as const, errors: [], data: null }; + const user = resultUserGet.ok ? resultUserGet.data : null; + + return ( +
+
+ +
+
+ ); +}; diff --git a/apps/storefront/src/app/[locale]/(main)/cart/page.tsx b/apps/storefront/src/app/[locale]/(main)/cart/page.tsx index c156dca7..c9acac61 100644 --- a/apps/storefront/src/app/[locale]/(main)/cart/page.tsx +++ b/apps/storefront/src/app/[locale]/(main)/cart/page.tsx @@ -14,10 +14,13 @@ import { deleteLineAction, updateLineQuantityAction, } from "./_actions/cart-actions"; +import { MarketplaceCartView } from "./_components/marketplace-cart-view"; export const generateMetadata = generateStandardCartMetadata; export default async function Page(props: any) { + const isMarketplaceEnabled = + process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; const [services, region, accessToken, checkoutId] = await Promise.all([ getServiceRegistry(), getCurrentRegion(), @@ -25,6 +28,27 @@ export default async function Page(props: any) { getCheckoutId(), ]); + if (isMarketplaceEnabled) { + return ( + + ); + } + return ( { - const [services, cartId, region, accessToken] = await Promise.all([ + const [services, region, accessToken] = await Promise.all([ getServiceRegistry(), - getCheckoutId(), getCurrentRegion(), getAccessToken(), ]); + const cartId = marketplaceEnabled + ? await getCheckoutIdForVendor(clientProductVendorId) + : await getCheckoutId(); // Call the pure function with services and context const result = await addToBag( @@ -40,8 +50,6 @@ export const addToBagAction = async ({ region, cartId, accessToken: accessToken ?? null, - marketplaceEnabled: - process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false", cacheTTL: { cart: services.config.cacheTTL.cart, }, @@ -50,13 +58,17 @@ export const addToBagAction = async ({ // Handle Next.js-specific side effects (cookies, revalidation) if (result.ok) { - if (!cartId) { + if (marketplaceEnabled) { + await setCheckoutIdForVendor(clientProductVendorId, result.data.cartId); + } else if (!cartId) { // Save the cartId in the cookie for future requests await setCheckoutIdCookie(result.data.cartId); } revalidateTag(`CHECKOUT:${cartId ?? result.data.cartId}`, "max"); revalidatePath(paths.cart.asPath()); + + return result; } storefrontLogger.error("Failed to add item to bag", { diff --git a/apps/storefront/src/features/checkout/aggregations.ts b/apps/storefront/src/features/checkout/aggregations.ts new file mode 100644 index 00000000..ec40c344 --- /dev/null +++ b/apps/storefront/src/features/checkout/aggregations.ts @@ -0,0 +1,148 @@ +import { type Cart } from "@nimara/domain/objects/Cart"; +import { type Checkout } from "@nimara/domain/objects/Checkout"; +import { type Price, type TaxedMoney } from "@nimara/domain/objects/common"; + +import { type MarketplaceCheckoutItem } from "@/features/checkout/types"; + +const sumPrices = (prices: Price[]): Price => { + if (!prices.length) { + return { + amount: 0, + currency: "USD", + }; + } + + const [firstPrice] = prices; + + return { + amount: prices.reduce((sum, price) => sum + price.amount, 0), + currency: firstPrice.currency, + }; +}; + +const sumTaxedMoney = (values: TaxedMoney[]): TaxedMoney => { + if (!values.length) { + return { + gross: { amount: 0, currency: "USD" }, + net: { amount: 0, currency: "USD" }, + tax: { amount: 0, currency: "USD" }, + }; + } + + return { + gross: sumPrices(values.map((value) => value.gross)), + net: sumPrices(values.map((value) => value.net)), + tax: sumPrices(values.map((value) => value.tax)), + }; +}; + +export const aggregateCarts = ( + carts: Array<{ cart: Cart; checkoutId: string }>, +): { + aggregatedCart: Cart; + lineCheckoutIdMap: Record; +} => { + const [firstCart] = carts; + + if (!firstCart) { + throw new Error("Cannot aggregate carts: no carts provided."); + } + + const lineCheckoutIdMap = Object.fromEntries( + carts.flatMap(({ cart, checkoutId }) => + cart.lines.map((line) => [line.id, checkoutId]), + ), + ); + + return { + aggregatedCart: { + id: firstCart.checkoutId, + lines: carts.flatMap(({ cart }) => cart.lines), + linesCount: carts.reduce((sum, { cart }) => sum + cart.linesCount, 0), + linesQuantityCount: carts.reduce( + (sum, { cart }) => sum + cart.linesQuantityCount, + 0, + ), + subtotal: sumPrices(carts.map(({ cart }) => cart.subtotal)), + total: sumPrices(carts.map(({ cart }) => cart.total)), + problems: { + insufficientStock: carts.flatMap( + ({ cart }) => cart.problems.insufficientStock, + ), + variantNotAvailable: carts.flatMap( + ({ cart }) => cart.problems.variantNotAvailable, + ), + }, + }, + lineCheckoutIdMap, + }; +}; + +export const aggregateMarketplaceCheckouts = ( + checkoutItems: MarketplaceCheckoutItem[], +): Checkout => { + const [firstCheckoutItem] = checkoutItems; + + if (!firstCheckoutItem) { + throw new Error("Cannot aggregate checkouts: no checkouts provided."); + } + + const checkouts = checkoutItems.map((item) => item.checkout); + const [firstCheckout] = checkouts; + + return { + ...firstCheckout, + lines: checkouts.flatMap((checkout) => checkout.lines), + subtotalPrice: sumTaxedMoney( + checkouts.map((checkout) => checkout.subtotalPrice), + ), + shippingPrice: sumTaxedMoney( + checkouts.map((checkout) => checkout.shippingPrice), + ), + totalPrice: sumTaxedMoney(checkouts.map((checkout) => checkout.totalPrice)), + discount: (() => { + const discounts = checkouts + .map((checkout) => checkout.discount) + .filter( + (discount): discount is NonNullable => !!discount, + ); + + if (!discounts.length) { + return null; + } + + return sumPrices(discounts); + })(), + problems: { + insufficientStock: checkouts.flatMap( + (checkout) => checkout.problems.insufficientStock, + ), + variantNotAvailable: checkouts.flatMap( + (checkout) => checkout.problems.variantNotAvailable, + ), + }, + isShippingRequired: checkouts.some( + (checkout) => checkout.isShippingRequired, + ), + shippingAddress: checkouts.every( + (checkout) => !checkout.isShippingRequired || checkout.shippingAddress, + ) + ? (checkouts.find((checkout) => checkout.shippingAddress) + ?.shippingAddress ?? null) + : null, + billingAddress: checkouts.every((checkout) => checkout.billingAddress) + ? (checkouts.find((checkout) => checkout.billingAddress) + ?.billingAddress ?? null) + : null, + email: checkouts.every((checkout) => checkout.email) + ? (checkouts.find((checkout) => checkout.email)?.email ?? null) + : null, + deliveryMethod: checkouts.every( + (checkout) => !checkout.isShippingRequired || checkout.deliveryMethod, + ) + ? (checkouts.find((checkout) => checkout.deliveryMethod) + ?.deliveryMethod ?? null) + : null, + voucherCode: null, + }; +}; diff --git a/apps/storefront/src/features/checkout/cart.ts b/apps/storefront/src/features/checkout/cart.ts index 5afb3502..5d112127 100644 --- a/apps/storefront/src/features/checkout/cart.ts +++ b/apps/storefront/src/features/checkout/cart.ts @@ -4,10 +4,123 @@ import { revalidatePath } from "next/cache"; import { cookies } from "next/headers"; import { COOKIE_KEY, COOKIE_MAX_AGE } from "@/config"; +import { MARKETPLACE_NO_VENDOR_BUCKET } from "@/features/checkout/constants"; import { revalidateTag } from "@/foundation/cache/cache"; import { paths } from "@/foundation/routing/paths"; import { getStorefrontLogger } from "@/services/lazy-logging"; +const MARKETPLACE_COOKIE_VERSION = 1; + +type MarketplaceCheckoutCookieV1 = { + checkouts: Record; + v: number; +}; + +const isMarketplaceEnabled = + process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; + +const sanitizeCheckoutId = (value: unknown): string | null => { + if (typeof value !== "string") { + return null; + } + + const normalized = value.trim(); + + return normalized.length > 0 ? normalized : null; +}; + +const normalizeVendorKey = (vendorKey: string | null | undefined): string => { + const normalized = vendorKey?.trim(); + + if (!normalized) { + return MARKETPLACE_NO_VENDOR_BUCKET; + } + + return normalized; +}; + +const parseCheckoutCookie = ( + cookieValue: string | undefined, +): { + checkoutIdsByVendor: Record; + legacyCheckoutId: string | null; +} => { + const normalizedCookieValue = cookieValue?.trim(); + + if (!normalizedCookieValue) { + return { + checkoutIdsByVendor: {}, + legacyCheckoutId: null, + }; + } + + try { + const parsed = JSON.parse( + normalizedCookieValue, + ) as MarketplaceCheckoutCookieV1 | null; + + const checkouts = parsed?.checkouts; + + if ( + typeof parsed?.v === "number" && + parsed.v === MARKETPLACE_COOKIE_VERSION && + typeof checkouts === "object" && + checkouts !== null + ) { + const sanitizedCheckouts = Object.fromEntries( + Object.entries(checkouts) + .map(([vendorKey, checkoutId]) => [ + normalizeVendorKey(vendorKey), + sanitizeCheckoutId(checkoutId), + ]) + .filter( + (entry): entry is [string, string] => + typeof entry[1] === "string" && entry[1].length > 0, + ), + ); + + return { + checkoutIdsByVendor: sanitizedCheckouts, + legacyCheckoutId: null, + }; + } + } catch { + // Ignore parse errors and fallback to legacy string handling. + } + + return { + checkoutIdsByVendor: {}, + legacyCheckoutId: sanitizeCheckoutId(normalizedCookieValue), + }; +}; + +const serializeMarketplaceCheckoutCookie = ( + checkoutIdsByVendor: Record, +): string => + JSON.stringify({ + v: MARKETPLACE_COOKIE_VERSION, + checkouts: checkoutIdsByVendor, + } satisfies MarketplaceCheckoutCookieV1); + +const getParsedCheckoutCookie = async () => { + const cookieStorage = await cookies(); + const cookieValue = cookieStorage.get(COOKIE_KEY.checkoutId)?.value; + + return parseCheckoutCookie(cookieValue); +}; + +const setCheckoutCookieRaw = async (value: string) => { + const cookieStorage = await cookies(); + + cookieStorage.set(COOKIE_KEY.checkoutId, value, { + path: "/", + maxAge: COOKIE_MAX_AGE.checkout, + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + }); +}; + /** * Revalidates the cart/checkout cache. * @param id - The checkout ID @@ -25,16 +138,8 @@ export const revalidateCart = async (id: string): Promise => { * @returns void */ export const setCheckoutIdCookie = async (id: string) => { + await setCheckoutCookieRaw(id); const cookieStorage = await cookies(); - - cookieStorage.set(COOKIE_KEY.checkoutId, id, { - path: "/", - maxAge: COOKIE_MAX_AGE.checkout, - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - }); - const checkoutIdCookie = cookieStorage.get(COOKIE_KEY.checkoutId); const storefrontLogger = await getStorefrontLogger(); @@ -50,12 +155,99 @@ export const setCheckoutIdCookie = async (id: string) => { } }; +export const setMarketplaceCheckoutIdsCookie = async ( + checkoutIdsByVendor: Record, +) => { + const normalizedCheckouts = Object.fromEntries( + Object.entries(checkoutIdsByVendor) + .map(([vendorKey, checkoutId]) => [ + normalizeVendorKey(vendorKey), + sanitizeCheckoutId(checkoutId), + ]) + .filter( + (entry): entry is [string, string] => + typeof entry[1] === "string" && entry[1].length > 0, + ), + ); + + if (Object.keys(normalizedCheckouts).length === 0) { + return clearCheckoutCookie(); + } + + await setCheckoutCookieRaw( + serializeMarketplaceCheckoutCookie(normalizedCheckouts), + ); +}; + +export const setCheckoutIdForVendor = async ( + vendorKey: string | null | undefined, + checkoutId: string, +) => { + const { checkoutIdsByVendor, legacyCheckoutId } = + await getParsedCheckoutCookie(); + const currentMarketplaceState = + Object.keys(checkoutIdsByVendor).length > 0 + ? checkoutIdsByVendor + : legacyCheckoutId + ? { + [MARKETPLACE_NO_VENDOR_BUCKET]: legacyCheckoutId, + } + : {}; + + currentMarketplaceState[normalizeVendorKey(vendorKey)] = checkoutId; + + await setMarketplaceCheckoutIdsCookie(currentMarketplaceState); +}; + +export const getCheckoutIdsByVendor = async (): Promise< + Record +> => { + const { checkoutIdsByVendor, legacyCheckoutId } = + await getParsedCheckoutCookie(); + + if (Object.keys(checkoutIdsByVendor).length > 0) { + return checkoutIdsByVendor; + } + + if (legacyCheckoutId) { + return { + [MARKETPLACE_NO_VENDOR_BUCKET]: legacyCheckoutId, + }; + } + + return {}; +}; + +export const getCheckoutIdForVendor = async ( + vendorKey: string | null | undefined, +): Promise => { + const checkoutIdsByVendor = await getCheckoutIdsByVendor(); + + return checkoutIdsByVendor[normalizeVendorKey(vendorKey)] ?? null; +}; + +export const getCheckoutIds = async (): Promise => { + if (!isMarketplaceEnabled) { + const checkoutId = await getCheckoutId(); + + return checkoutId ? [checkoutId] : []; + } + + const checkoutIdsByVendor = await getCheckoutIdsByVendor(); + + return [...new Set(Object.values(checkoutIdsByVendor))]; +}; + /** * Gets the checkout ID from the cookie. * @returns The checkout ID from the cookie, or null if not found. */ export const getCheckoutId = async (): Promise => { - const checkoutId = (await cookies()).get(COOKIE_KEY.checkoutId)?.value; + const { checkoutIdsByVendor, legacyCheckoutId } = + await getParsedCheckoutCookie(); + + const checkoutId = + legacyCheckoutId ?? Object.values(checkoutIdsByVendor)[0] ?? null; // If the checkout ID is not found, return null if (!checkoutId) { @@ -68,3 +260,9 @@ export const getCheckoutId = async (): Promise => { return checkoutId; }; + +export const clearCheckoutCookie = async () => { + const cookieStorage = await cookies(); + + cookieStorage.delete(COOKIE_KEY.checkoutId); +}; diff --git a/apps/storefront/src/features/checkout/checkout-actions.ts b/apps/storefront/src/features/checkout/checkout-actions.ts index a2167e84..6a862635 100644 --- a/apps/storefront/src/features/checkout/checkout-actions.ts +++ b/apps/storefront/src/features/checkout/checkout-actions.ts @@ -4,8 +4,16 @@ import { getLocale } from "next-intl/server"; import { type Checkout } from "@nimara/domain/objects/Checkout"; import { redirect } from "@nimara/i18n/routing"; -import { getCheckoutId } from "@/features/checkout/cart"; +import { aggregateMarketplaceCheckouts } from "@/features/checkout/aggregations"; +import { + clearCheckoutCookie, + getCheckoutId, + getCheckoutIdsByVendor, + setMarketplaceCheckoutIdsCookie, +} from "@/features/checkout/cart"; import { deleteCheckoutIdCookie } from "@/features/checkout/checkout"; +import { MARKETPLACE_NO_VENDOR_BUCKET } from "@/features/checkout/constants"; +import { type MarketplaceCheckoutItem } from "@/features/checkout/types"; import { getCurrentRegion } from "@/foundation/regions"; import { paths } from "@/foundation/routing/paths"; import { getServiceRegistry } from "@/services/registry"; @@ -39,6 +47,108 @@ export const getCheckoutOrRedirect = async (): Promise | never => { return resultCheckout.data.checkout; }; +const getVendorDisplayName = ( + checkout: Checkout, + vendorKey: string, +): string => { + const vendorFromLines = checkout.lines + .map((line) => line.product.vendorId) + .find((vendorId): vendorId is string => !!vendorId); + + if (vendorFromLines) { + return vendorFromLines; + } + + if (vendorKey === MARKETPLACE_NO_VENDOR_BUCKET) { + return "No vendor"; + } + + return vendorKey; +}; + +export const getMarketplaceCheckoutsOrRedirect = async (): + | Promise + | never => { + const [checkoutIdsByVendor, locale, region, services] = await Promise.all([ + getCheckoutIdsByVendor(), + getLocale(), + getCurrentRegion(), + getServiceRegistry(), + ]); + + const checkoutIds = [...new Set(Object.values(checkoutIdsByVendor))]; + + if (!checkoutIds.length) { + redirect({ href: paths.cart.asPath(), locale }); + } + + const checkoutService = await services.getCheckoutService(); + const checkoutIdToVendorKey = new Map( + Object.entries(checkoutIdsByVendor).map(([vendorKey, checkoutId]) => [ + checkoutId, + vendorKey, + ]), + ); + const resultCheckouts = await Promise.all( + checkoutIds.map(async (checkoutId) => { + const result = await checkoutService.checkoutGet({ + checkoutId, + languageCode: region.language.code, + countryCode: region.market.countryCode, + }); + + if (!result.ok) { + return null; + } + + const vendorKey = + checkoutIdToVendorKey.get(checkoutId) ?? MARKETPLACE_NO_VENDOR_BUCKET; + const checkout = result.data.checkout; + + await validateCheckoutLinesAction({ checkout, locale }); + + return { + checkout, + checkoutId, + vendorKey, + vendorDisplayName: getVendorDisplayName(checkout, vendorKey), + } satisfies MarketplaceCheckoutItem; + }), + ); + + const validCheckoutItems = resultCheckouts.filter( + (entry): entry is MarketplaceCheckoutItem => + entry !== null && entry.checkout.lines.length > 0, + ); + + if (!validCheckoutItems.length) { + await clearCheckoutCookie(); + redirect({ href: paths.cart.asPath(), locale }); + } + + const validCheckoutIds = new Set( + validCheckoutItems.map((item) => item.checkoutId), + ); + const filteredCheckoutIdsByVendor = Object.fromEntries( + Object.entries(checkoutIdsByVendor).filter(([, checkoutId]) => + validCheckoutIds.has(checkoutId), + ), + ); + + if ( + Object.keys(filteredCheckoutIdsByVendor).length !== + Object.keys(checkoutIdsByVendor).length + ) { + await setMarketplaceCheckoutIdsCookie(filteredCheckoutIdsByVendor); + } + + return validCheckoutItems; +}; + +export const getMarketplaceCheckoutSummary = ( + checkoutItems: MarketplaceCheckoutItem[], +): Checkout => aggregateMarketplaceCheckouts(checkoutItems); + /** * Validates checkout lines and redirects to the cart page if there are issues. */ diff --git a/apps/storefront/src/features/checkout/constants.ts b/apps/storefront/src/features/checkout/constants.ts new file mode 100644 index 00000000..04b65c71 --- /dev/null +++ b/apps/storefront/src/features/checkout/constants.ts @@ -0,0 +1 @@ +export const MARKETPLACE_NO_VENDOR_BUCKET = "__NO_VENDOR__"; diff --git a/apps/storefront/src/features/checkout/summary.tsx b/apps/storefront/src/features/checkout/summary.tsx index e5ecde0d..159c1cbf 100644 --- a/apps/storefront/src/features/checkout/summary.tsx +++ b/apps/storefront/src/features/checkout/summary.tsx @@ -10,6 +10,7 @@ interface SummaryProps { promoCode: string; }) => Promise>; checkout: Checkout; + hidePromoCode?: boolean; removePromoCodeAction?: (params: { checkoutId: string; promoCode: string; @@ -20,6 +21,7 @@ export const Summary = ({ checkout, addPromoCodeAction, removePromoCodeAction, + hidePromoCode = false, }: SummaryProps) => { return ( @@ -29,11 +31,13 @@ export const Summary = ({ isLinesEditable={false} problems={checkout.problems} /> - + {!hidePromoCode && ( + + )} { const resultUserGet = await userService.userGet(accessToken); const user = resultUserGet.ok ? resultUserGet.data : null; + const isMarketplaceEnabled = + process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; let checkoutLinesCount = 0; - const checkoutId = await getCheckoutId(); + const checkoutIds = isMarketplaceEnabled + ? await getCheckoutIds() + : [await getCheckoutId()].filter( + (checkoutId): checkoutId is string => !!checkoutId, + ); - if (checkoutId) { + if (checkoutIds.length) { const cartService = await services.getCartService(); - const resultCartGet = await cartService.cartGet({ - cartId: checkoutId, - languageCode: region.language.code, - countryCode: region.market.countryCode, - options: { - next: { - tags: [`CHECKOUT:${checkoutId}`], - revalidate: CACHE_TTL.cart, - }, - }, - }); - - checkoutLinesCount = resultCartGet.data?.linesQuantityCount ?? 0; + const cartResults = await Promise.all( + checkoutIds.map((checkoutId) => + cartService.cartGet({ + cartId: checkoutId, + languageCode: region.language.code, + countryCode: region.market.countryCode, + options: { + next: { + tags: [`CHECKOUT:${checkoutId}`], + revalidate: CACHE_TTL.cart, + }, + }, + }), + ), + ); + + checkoutLinesCount = cartResults.reduce((count, resultCartGet) => { + if (!resultCartGet.ok) { + return count; + } + + return count + (resultCartGet.data?.linesQuantityCount ?? 0); + }, 0); } const shoppingBag = ; diff --git a/apps/storefront/src/foundation/auth/login.ts b/apps/storefront/src/foundation/auth/login.ts index 00e821b6..fd9e2fff 100644 --- a/apps/storefront/src/foundation/auth/login.ts +++ b/apps/storefront/src/foundation/auth/login.ts @@ -5,7 +5,12 @@ import { AuthError } from "next-auth"; import { signIn } from "@/auth"; import { CACHE_TTL } from "@/config"; -import { getCheckoutId, setCheckoutIdCookie } from "@/features/checkout/cart"; +import { + getCheckoutId, + getCheckoutIds, + setCheckoutIdCookie, + setCheckoutIdForVendor, +} from "@/features/checkout/cart"; import { getCurrentRegion } from "@/foundation/regions"; import { paths } from "@/foundation/routing/paths"; import { errorService } from "@/services/error"; @@ -22,15 +27,19 @@ export async function login({ redirectUrl?: string; }) { try { + const isMarketplaceEnabled = + process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; + await signIn("credentials", { email, password, redirect: false, }); - const [accessToken, checkoutId, services] = await Promise.all([ + const [accessToken, checkoutId, checkoutIds, services] = await Promise.all([ getAccessToken(), getCheckoutId(), + getCheckoutIds(), getServiceRegistry(), ]); const [checkoutService, userService] = await Promise.all([ @@ -41,7 +50,20 @@ export async function login({ const resultUserGet = await userService.userGet(accessToken); const user = resultUserGet.ok ? resultUserGet.data : null; - if (user?.checkoutIds.length) { + if (isMarketplaceEnabled) { + if (checkoutIds.length > 0) { + await Promise.all( + checkoutIds.map((checkoutId) => + checkoutService.checkoutCustomerAttach({ + accessToken, + id: checkoutId, + }), + ), + ); + } else if (user?.checkoutIds.length) { + await setCheckoutIdForVendor(null, user.checkoutIds[0]); + } + } else if (user?.checkoutIds.length) { const userLatestCheckoutId = user.checkoutIds[0]; await setCheckoutIdCookie(userLatestCheckoutId); diff --git a/apps/storefront/src/foundation/checkout/actions/update-checkout-address-action.ts b/apps/storefront/src/foundation/checkout/actions/update-checkout-address-action.ts index c7edcfac..4d7b0fd6 100644 --- a/apps/storefront/src/foundation/checkout/actions/update-checkout-address-action.ts +++ b/apps/storefront/src/foundation/checkout/actions/update-checkout-address-action.ts @@ -9,6 +9,7 @@ import { import { type Checkout } from "@nimara/domain/objects/Checkout"; import { type AsyncResult, ok } from "@nimara/domain/objects/Result"; +import { getCheckoutIds } from "@/features/checkout/cart"; import { paths } from "@/foundation/routing/paths"; import { getServiceRegistry } from "@/services/registry"; @@ -29,6 +30,8 @@ export const updateCheckoutAddressAction = async ({ }): AsyncResult<{ success: true; }> => { + const isMarketplaceEnabled = + process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; const services = await getServiceRegistry(); const checkoutService = await services.getCheckoutService(); @@ -37,10 +40,28 @@ export const updateCheckoutAddressAction = async ({ ? checkoutService.checkoutShippingAddressUpdate : checkoutService.checkoutBillingAddressUpdate; - const result = await updateFn(values); + if (isMarketplaceEnabled) { + const checkoutIds = await getCheckoutIds(); + const targetCheckoutIds = checkoutIds.length ? checkoutIds : [values.id]; + const results = await Promise.all( + targetCheckoutIds.map((id) => + updateFn({ + ...values, + id, + }), + ), + ); + const failedResult = results.find((result) => !result.ok); - if (!result.ok) { - return result; + if (failedResult && !failedResult.ok) { + return failedResult; + } + } else { + const result = await updateFn(values); + + if (!result.ok) { + return result; + } } revalidatePath(paths.checkout.asPath()); diff --git a/apps/storefront/src/foundation/checkout/sections/delivery-method/actions.ts b/apps/storefront/src/foundation/checkout/sections/delivery-method/actions.ts index 0e0c8b5a..c786db17 100644 --- a/apps/storefront/src/foundation/checkout/sections/delivery-method/actions.ts +++ b/apps/storefront/src/foundation/checkout/sections/delivery-method/actions.ts @@ -34,3 +34,44 @@ export const updateCheckoutDeliveryMethod = async ({ redirectUrl: paths.checkout.asPath({ query: { step: "payment" } }), }); }; + +export const updateMarketplaceDeliveryMethods = async ({ + selectionsByCheckoutId, +}: { + selectionsByCheckoutId: Record; +}): AsyncResult<{ + redirectUrl: string; +}> => { + const entries = Object.entries(selectionsByCheckoutId).filter( + (entry): entry is [string, string] => + !!entry[0] && typeof entry[1] === "string" && entry[1].length > 0, + ); + + if (!entries.length) { + return { + ok: false, + errors: [{ code: "CHECKOUT_DELIVERY_METHOD_UPDATE_ERROR" }], + }; + } + + const results = await Promise.all( + entries.map(([checkoutId, deliveryMethodId]) => + updateDeliveryMethodAction({ + id: checkoutId, + deliveryMethodId, + }), + ), + ); + + const failedResult = results.find((result) => !result.ok); + + if (failedResult && !failedResult.ok) { + return failedResult; + } + + revalidatePath(paths.checkout.asPath()); + + return ok({ + redirectUrl: paths.checkout.asPath({ query: { step: "payment" } }), + }); +}; diff --git a/apps/storefront/src/foundation/checkout/sections/delivery-method/marketplace-form.tsx b/apps/storefront/src/foundation/checkout/sections/delivery-method/marketplace-form.tsx new file mode 100644 index 00000000..b57c9063 --- /dev/null +++ b/apps/storefront/src/foundation/checkout/sections/delivery-method/marketplace-form.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { useFormatter, useTranslations } from "next-intl"; +import { useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; + +import { type AppErrorCode } from "@nimara/domain/objects/Error"; +import { RadioFormGroup } from "@nimara/foundation/form-components/radio-form-group"; +import { Button } from "@nimara/ui/components/button"; + +import { type MarketplaceCheckoutItem } from "@/features/checkout/types"; +import { useRouterWithState } from "@/foundation/use-router-with-state"; + +import { updateMarketplaceDeliveryMethods } from "./actions"; +import { getDeliveryMethodOptions } from "./helpers/get-delivery-method-options"; + +interface MarketplaceDeliveryMethodFormProps { + checkoutItems: MarketplaceCheckoutItem[]; + onComplete?: () => void; +} + +export const MarketplaceDeliveryMethodForm = ({ + checkoutItems, + onComplete, +}: MarketplaceDeliveryMethodFormProps) => { + const t = useTranslations(); + const formatter = useFormatter(); + const { isRedirecting, push } = useRouterWithState(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [globalErrors, setGlobalErrors] = useState([]); + const form = useForm>({ + defaultValues: Object.fromEntries( + checkoutItems.map((item) => [ + `deliveryMethod-${item.checkoutId}`, + item.checkout.deliveryMethod?.id ?? + item.checkout.shippingMethods[0]?.id ?? + "", + ]), + ), + }); + + const isDisabled = isRedirecting || isSubmitting; + + const onSubmit = async (values: Record) => { + if (isDisabled) { + return; + } + + setGlobalErrors([]); + setIsSubmitting(true); + + const selectionsByCheckoutId = Object.fromEntries( + checkoutItems.map((item) => [ + item.checkoutId, + values[`deliveryMethod-${item.checkoutId}`] ?? "", + ]), + ); + + const result = await updateMarketplaceDeliveryMethods({ + selectionsByCheckoutId, + }); + + if (!result.ok) { + setGlobalErrors(result.errors.map((error) => error.code)); + setIsSubmitting(false); + + return; + } + + if (onComplete) { + onComplete(); + } else { + push(result.data.redirectUrl); + } + }; + + return ( + +
+ {checkoutItems.map(({ checkout, checkoutId, vendorDisplayName }) => ( +
+

+ Vendor: {vendorDisplayName} +

+ + formatter.number(method.price.amount, { + style: "currency", + currency: method.price.currency, + }), + })} + > + {checkout.shippingMethods.map((method) => ( +
+

{method.name}

+

+ {formatter.number(method.price.amount, { + style: "currency", + currency: method.price.currency, + })} +

+
+ ))} +
+
+ ))} + + {globalErrors.length > 0 && ( +
+ {globalErrors.map((errorCode, index) => ( +

+ {t(`errors.${errorCode}`)} +

+ ))} +
+ )} + + +
+
+ ); +}; diff --git a/apps/storefront/src/foundation/checkout/sections/payment/actions.ts b/apps/storefront/src/foundation/checkout/sections/payment/actions.ts index 42dc4d95..eaa0e277 100644 --- a/apps/storefront/src/foundation/checkout/sections/payment/actions.ts +++ b/apps/storefront/src/foundation/checkout/sections/payment/actions.ts @@ -4,9 +4,10 @@ import { revalidatePath } from "next/cache"; import { type AllCountryCode } from "@nimara/domain/consts"; import { type Checkout } from "@nimara/domain/objects/Checkout"; -import { type AsyncResult } from "@nimara/domain/objects/Result"; +import { type AsyncResult, err, ok } from "@nimara/domain/objects/Result"; import { schemaToAddress } from "@nimara/foundation/address/address"; +import { clientEnvs } from "@/envs/client"; import { createAddressAction } from "@/foundation/address/create-address-action"; import { updateCheckoutAddressAction } from "@/foundation/checkout/actions/update-checkout-address-action"; import { paths } from "@/foundation/routing/paths"; @@ -104,3 +105,86 @@ export const initializePaymentTransaction = async ({ saveForFutureUse, }); }; + +export const initializeMarketplacePaymentIntent = async ({ + buyerId, + checkouts, +}: { + buyerId?: string; + checkouts: Array<{ + amount: number; + checkoutId: string; + currency: string; + }>; +}): AsyncResult<{ clientSecret: string }> => { + const marketplaceVendorUrl = process.env.NEXT_PUBLIC_MARKETPLACE_VENDOR_URL; + + if (!marketplaceVendorUrl) { + return err([{ code: "GENERIC_PAYMENT_ERROR" }]); + } + + const normalizedBaseUrl = marketplaceVendorUrl.startsWith("http") + ? marketplaceVendorUrl + : `https://${marketplaceVendorUrl}`; + + let saleorDomain: string; + + try { + saleorDomain = new URL(clientEnvs.NEXT_PUBLIC_SALEOR_API_URL).hostname; + } catch { + return err([{ code: "GENERIC_PAYMENT_ERROR" }]); + } + + try { + const response = await fetch( + `${normalizedBaseUrl.replace(/\/$/, "")}/api/payments/payment-intent`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-saleor-domain": saleorDomain, + }, + body: JSON.stringify({ + checkouts, + buyerId, + }), + cache: "no-store", + }, + ); + + if (!response.ok) { + let responseBody = ""; + + try { + responseBody = await response.text(); + } catch { + responseBody = ""; + } + + storefrontLogger.error( + "Marketplace payment intent initialization failed", + { + checkoutsCount: checkouts.length, + responseBodyPreview: responseBody.slice(0, 600), + saleorDomain, + status: response.status, + statusText: response.statusText, + }, + ); + + return err([{ code: "GENERIC_PAYMENT_ERROR" }]); + } + + const payload = (await response.json()) as { clientSecret?: string }; + + if (!payload.clientSecret) { + return err([{ code: "GENERIC_PAYMENT_ERROR" }]); + } + + return ok({ + clientSecret: payload.clientSecret, + }); + } catch { + return err([{ code: "GENERIC_PAYMENT_ERROR" }]); + } +}; diff --git a/apps/storefront/src/foundation/checkout/sections/payment/payment.tsx b/apps/storefront/src/foundation/checkout/sections/payment/payment.tsx index 0d7fe3f2..ec659752 100644 --- a/apps/storefront/src/foundation/checkout/sections/payment/payment.tsx +++ b/apps/storefront/src/foundation/checkout/sections/payment/payment.tsx @@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { LockIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import { useTheme } from "next-themes"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { FormProvider, type SubmitHandler, useForm } from "react-hook-form"; import { type AllCountryCode } from "@nimara/domain/consts"; @@ -37,7 +37,11 @@ import { cn } from "@nimara/ui/lib/utils"; import { PAYMENT_ELEMENT_ID } from "@/features/checkout/consts"; import { PaymentMethods } from "@/features/checkout/payment-methods"; -import { updateBillingAddress } from "@/foundation/checkout/sections/payment/actions"; +import { type MarketplaceCheckoutItem } from "@/features/checkout/types"; +import { + initializeMarketplacePaymentIntent, + updateBillingAddress, +} from "@/foundation/checkout/sections/payment/actions"; import { type BillingAddressPath, type BillingAddressValue, @@ -63,12 +67,37 @@ type PaymentProps = { countryCode: AllCountryCode; errorCode?: AppErrorCode; formattedAddresses: FormattedAddress[]; + marketplaceCheckouts?: MarketplaceCheckoutItem[]; paymentGatewayCustomer: Maybe; paymentGatewayMethods: PaymentMethod[]; storeUrl: string; user: User | null; }; +type MarketplaceIntentCheckout = { + amount: number; + checkoutId: string; + currency: string; +}; + +const buildMarketplaceIntentKey = ({ + buyerId, + checkouts, +}: { + buyerId?: string; + checkouts: MarketplaceIntentCheckout[]; +}) => + JSON.stringify({ + buyerId: buyerId ?? null, + checkouts: [...checkouts] + .sort((a, b) => a.checkoutId.localeCompare(b.checkoutId)) + .map((checkout) => ({ + amount: checkout.amount, + checkoutId: checkout.checkoutId, + currency: checkout.currency.toUpperCase(), + })), + }); + export const Payment = ({ checkout, errorCode, @@ -79,6 +108,7 @@ export const Payment = ({ paymentGatewayMethods, paymentGatewayCustomer, formattedAddresses, + marketplaceCheckouts, user, }: PaymentProps) => { const t = useTranslations(); @@ -93,7 +123,12 @@ export const Payment = ({ const [isInitialized, setIsInitialized] = useState(false); const [isMounted, setIsMounted] = useState(false); const [isCountryChanging, setIsCountryChanging] = useState(false); - const hasSavedPaymentMethods = paymentGatewayMethods.length > 0; + const isMarketplacePayment = + process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false" && + !!marketplaceCheckouts && + marketplaceCheckouts.length > 0; + const hasSavedPaymentMethods = + !isMarketplacePayment && paymentGatewayMethods.length > 0; const [paymentMethodTab, setPaymentMethodTab] = useState( hasSavedPaymentMethods ? "saved" : "new", ); @@ -103,6 +138,10 @@ export const Payment = ({ const [errors, setErrors] = useState( errorCode ? [errorCode] : [], ); + const [paymentElementSecret, setPaymentElementSecret] = + useState>(undefined); + const marketplaceIntentInFlightRef = useRef(null); + const marketplaceIntentInitializedRef = useRef(null); const defaultBillingAddress = formattedAddresses.find( ({ address }) => address.isDefaultBillingAddress, @@ -124,6 +163,43 @@ export const Payment = ({ const hasDefaultBillingAddressInCurrentChannel = supportedCountryCodesInChannel.includes(defaultBillingAddress?.country); const saveAddressForFutureUse = !!(user && addressActiveTab === "new"); + const marketplaceIntentCheckouts = useMemo< + MarketplaceIntentCheckout[] + >(() => { + if (!isMarketplacePayment) { + return []; + } + + return ( + marketplaceCheckouts?.map((item) => ({ + checkoutId: item.checkoutId, + amount: item.checkout.totalPrice.gross.amount, + currency: item.checkout.totalPrice.gross.currency, + })) ?? [ + { + checkoutId: checkout.id, + amount: checkout.totalPrice.gross.amount, + currency: checkout.totalPrice.gross.currency, + }, + ] + ); + }, [ + checkout.id, + checkout.totalPrice.gross.amount, + checkout.totalPrice.gross.currency, + isMarketplacePayment, + marketplaceCheckouts, + ]); + const marketplaceIntentKey = useMemo(() => { + if (!isMarketplacePayment || marketplaceIntentCheckouts.length === 0) { + return null; + } + + return buildMarketplaceIntentKey({ + buyerId: user?.id, + checkouts: marketplaceIntentCheckouts, + }); + }, [isMarketplacePayment, marketplaceIntentCheckouts, user?.id]); const form = useForm({ resolver: zodResolver(paymentSchema({ t, addressFormRows })), @@ -136,9 +212,10 @@ export const Payment = ({ : false, saveAddressForFutureUse, saveForFutureUse: !!user, - paymentMethod: - paymentGatewayMethods.find(({ isDefault }) => isDefault)?.id ?? - paymentGatewayMethods?.[0]?.id, + paymentMethod: isMarketplacePayment + ? undefined + : (paymentGatewayMethods.find(({ isDefault }) => isDefault)?.id ?? + paymentGatewayMethods?.[0]?.id), }, }); @@ -154,7 +231,11 @@ export const Payment = ({ const isLoading = !isInitialized || isProcessing; const canProceed = !isLoading && - (isAddingNewPaymentMethod ? isMounted : hasSelectedPaymentMethod); + (isMarketplacePayment + ? isMounted + : isAddingNewPaymentMethod + ? isMounted + : hasSelectedPaymentMethod); const isDark = resolvedTheme === "dark"; @@ -211,7 +292,7 @@ export const Payment = ({ * paymentExecute. */ - if (paymentMethod) { + if (!isMarketplacePayment && paymentMethod) { const result = await paymentService.paymentGatewayTransactionInitialize({ id: checkout.id, amount: checkout.totalPrice.gross.amount, @@ -249,27 +330,48 @@ export const Payment = ({ void (async () => { const paymentService = await paymentServiceLoader(); + if (isMarketplacePayment) { + await paymentService.paymentInitialize(); + setIsInitialized(true); + + return; + } + const [result] = await Promise.all([ paymentService.paymentGatewayInitialize({ id: checkout.id, amount: checkout.totalPrice.gross.amount, }), - await paymentService.paymentInitialize(), + paymentService.paymentInitialize(), ]); if (!result.ok) { setErrors(result.errors.map(({ code }) => code)); - } else { - setIsInitialized(true); + + return; } + + setIsInitialized(true); })(); - }, []); + }, [checkout.id, checkout.totalPrice.gross.amount, isMarketplacePayment]); useEffect(() => { - if (!isInitialized) { + if (isAddingNewPaymentMethod) { + form.setValue("paymentMethod", undefined); + return; } + setIsMounted(false); + }, [form, isAddingNewPaymentMethod]); + + useEffect(() => { + if (!isInitialized || !isAddingNewPaymentMethod) { + return; + } + + let isCancelled = false; + void (async () => { const paymentService = await paymentServiceLoader(); @@ -277,59 +379,135 @@ export const Payment = ({ * Using new payment method requires an new intent secret which is then passed * to paymentElementCreate. */ - if (isAddingNewPaymentMethod) { - let secret: string; + if (isMarketplacePayment) { + if (!marketplaceIntentKey || marketplaceIntentCheckouts.length === 0) { + return; + } - { - const result = - await paymentService.paymentGatewayTransactionInitialize({ - id: checkout.id, - amount: checkout.totalPrice.gross.amount, - customerId: paymentGatewayCustomer, - saveForFutureUse, - }); + if (marketplaceIntentInitializedRef.current === marketplaceIntentKey) { + return; + } - if (!result.ok) { - return setErrors(result.errors.map(({ code }) => code)); - } else { - secret = result.data.clientSecret; - } + if (marketplaceIntentInFlightRef.current === marketplaceIntentKey) { + return; } - const data = await paymentService.paymentElementCreate({ - locale: region.language.locale, - secret, - appearance: { - theme: isDark ? "night" : "flat", - variables: { - borderRadius: "5px", - }, - }, - options: { - layout: { - type: "accordion", - paymentMethodLogoPosition: "start", - defaultCollapsed: false, - }, - }, + marketplaceIntentInFlightRef.current = marketplaceIntentKey; + setIsMounted(false); + setPaymentElementSecret(undefined); + + const result = await initializeMarketplacePaymentIntent({ + buyerId: user?.id, + checkouts: marketplaceIntentCheckouts, }); - if (document.getElementById(PAYMENT_ELEMENT_ID)) { - data.mount(`#${PAYMENT_ELEMENT_ID}`); + if (marketplaceIntentInFlightRef.current === marketplaceIntentKey) { + marketplaceIntentInFlightRef.current = null; + } - setIsMounted(true); + if (!result.ok) { + setErrors(result.errors.map(({ code }) => code)); + + return; } + + marketplaceIntentInitializedRef.current = marketplaceIntentKey; + setPaymentElementSecret(result.data.clientSecret); + + return; } + + setIsMounted(false); + setPaymentElementSecret(undefined); + + const result = await paymentService.paymentGatewayTransactionInitialize({ + id: checkout.id, + amount: checkout.totalPrice.gross.amount, + customerId: paymentGatewayCustomer, + saveForFutureUse, + }); + + if (isCancelled) { + return; + } + + if (!result.ok) { + setErrors(result.errors.map(({ code }) => code)); + + return; + } + + setPaymentElementSecret(result.data.clientSecret); })(); - if (isAddingNewPaymentMethod) { - form.setValue("paymentMethod", undefined); - } - }, [paymentMethodTab, saveForFutureUse, isInitialized, resolvedTheme]); + return () => { + isCancelled = true; + }; + }, [ + checkout.id, + checkout.totalPrice.gross.amount, + isInitialized, + isAddingNewPaymentMethod, + isMarketplacePayment, + marketplaceIntentCheckouts, + marketplaceIntentKey, + paymentGatewayCustomer, + saveForFutureUse, + user?.id, + ]); useEffect(() => { + if (!isInitialized || !isAddingNewPaymentMethod || !paymentElementSecret) { + return; + } + + let isCancelled = false; + let unmountPaymentElement: (() => void) | undefined; + setIsMounted(false); - }, [saveForFutureUse, paymentMethodTab]); + + void (async () => { + const paymentService = await paymentServiceLoader(); + const data = await paymentService.paymentElementCreate({ + locale: region.language.locale, + secret: paymentElementSecret, + appearance: { + theme: isDark ? "night" : "flat", + variables: { + borderRadius: "5px", + }, + }, + options: { + layout: { + type: "accordion", + paymentMethodLogoPosition: "start", + defaultCollapsed: false, + }, + }, + }); + + if (isCancelled) { + return; + } + + if (document.getElementById(PAYMENT_ELEMENT_ID)) { + data.mount(`#${PAYMENT_ELEMENT_ID}`); + unmountPaymentElement = data.unmount; + setIsMounted(true); + } + })(); + + return () => { + isCancelled = true; + unmountPaymentElement?.(); + }; + }, [ + isAddingNewPaymentMethod, + isDark, + isInitialized, + paymentElementSecret, + region.language.locale, + ]); useEffect(() => { if (errorCode) { diff --git a/apps/storefront/src/foundation/checkout/sections/sections.tsx b/apps/storefront/src/foundation/checkout/sections/sections.tsx index 4ccb1232..2dfdb0b3 100644 --- a/apps/storefront/src/foundation/checkout/sections/sections.tsx +++ b/apps/storefront/src/foundation/checkout/sections/sections.tsx @@ -9,12 +9,14 @@ import { Card } from "@nimara/ui/components/card"; import { Separator } from "@nimara/ui/components/separator"; import { clientEnvs } from "@/envs/client"; +import { type MarketplaceCheckoutItem } from "@/features/checkout/types"; import { Payment } from "@/foundation/checkout/sections/payment/payment"; import { CheckoutPaymentSection } from "@/foundation/checkout/sections/payment/section"; import { paths } from "@/foundation/routing/paths"; import { type CheckoutStep } from "../steps"; import { DeliveryMethodForm } from "./delivery-method/form"; +import { MarketplaceDeliveryMethodForm } from "./delivery-method/marketplace-form"; import { CheckoutDeliveryMethodSection } from "./delivery-method/section"; import { type PaymentSectionData } from "./payment/types"; import { ShippingAddressForm } from "./shipping-address/form"; @@ -25,6 +27,7 @@ import { CheckoutUserDetailsSection } from "./user-details/section"; interface Props { checkout: Checkout; + marketplaceCheckouts?: MarketplaceCheckoutItem[]; paymentSectionData: PaymentSectionData | null; shippingAddressSectionData: ShippingAddressSectionData | null; step: CheckoutStep; @@ -34,24 +37,68 @@ interface Props { export const CheckoutSections = ({ step, checkout, + marketplaceCheckouts, paymentSectionData, shippingAddressSectionData, user, }: Props) => { const t = useTranslations(); const router = useRouter(); + const isMarketplaceFlow = + clientEnvs.NEXT_PUBLIC_MARKETPLACE_ENABLED && + !!marketplaceCheckouts && + marketplaceCheckouts.length > 0; + const checkoutCollection = isMarketplaceFlow + ? marketplaceCheckouts.map((item) => item.checkout) + : [checkout]; + const emailProvidedForAll = checkoutCollection.every( + (entry) => entry.email !== null, + ); + const shippingAddressProvidedForAll = checkoutCollection.every( + (entry) => !entry.isShippingRequired || entry.shippingAddress !== null, + ); + const deliveryMethodProvidedForAll = checkoutCollection.every( + (entry) => !entry.isShippingRequired || entry.deliveryMethod !== null, + ); + const isShippingRequiredForAny = checkoutCollection.some( + (entry) => entry.isShippingRequired, + ); + const marketplaceShippingCheckouts = marketplaceCheckouts?.filter( + (item) => item.checkout.isShippingRequired, + ); + const checkoutForSections: Checkout = { + ...checkout, + email: emailProvidedForAll + ? (checkout.email ?? + checkoutCollection.find((entry) => entry.email !== null)?.email ?? + null) + : null, + isShippingRequired: isShippingRequiredForAny, + shippingAddress: shippingAddressProvidedForAll + ? (checkout.shippingAddress ?? + checkoutCollection.find((entry) => entry.shippingAddress !== null) + ?.shippingAddress ?? + null) + : null, + deliveryMethod: deliveryMethodProvidedForAll + ? (checkout.deliveryMethod ?? + checkoutCollection.find((entry) => entry.deliveryMethod !== null) + ?.deliveryMethod ?? + null) + : null, + }; return ( { - const nextStep = checkout.isShippingRequired + const nextStep = checkoutForSections.isShippingRequired ? "shipping-address" : "payment"; @@ -66,16 +113,16 @@ export const CheckoutSections = ({ - {checkout.isShippingRequired && shippingAddressSectionData && ( + {checkoutForSections.isShippingRequired && shippingAddressSectionData && ( )} - {checkout.isShippingRequired && ( + {checkoutForSections.isShippingRequired && ( <> - { - router.push( - paths.checkout.asPath({ - query: { step: "payment" }, - }), - ); - }} - /> + {isMarketplaceFlow && marketplaceShippingCheckouts ? ( + { + router.push( + paths.checkout.asPath({ + query: { step: "payment" }, + }), + ); + }} + /> + ) : ( + { + router.push( + paths.checkout.asPath({ + query: { step: "payment" }, + }), + ); + }} + /> + )} )} - + {paymentSectionData ? ( diff --git a/apps/storefront/src/foundation/checkout/sections/user-details/actions.ts b/apps/storefront/src/foundation/checkout/sections/user-details/actions.ts index 5b780347..f5139e7c 100644 --- a/apps/storefront/src/foundation/checkout/sections/user-details/actions.ts +++ b/apps/storefront/src/foundation/checkout/sections/user-details/actions.ts @@ -3,8 +3,10 @@ import { revalidatePath } from "next/cache"; import { type Checkout } from "@nimara/domain/objects/Checkout"; +import { ok } from "@nimara/domain/objects/Result"; import { serverEnvs } from "@/envs/server"; +import { getCheckoutIds } from "@/features/checkout/cart"; import { paths } from "@/foundation/routing/paths"; import { getServiceRegistry } from "@/services/registry"; @@ -45,13 +47,43 @@ export const updateCheckoutUserDetailsAction = async ( payload: UpdateCheckoutUserDetailsPayload, opts: UpdateCheckoutUserDetailsOpts = {}, ) => { + const isMarketplaceEnabled = + process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; const services = await getServiceRegistry(); const checkoutService = await services.getCheckoutService(); - const result = await checkoutService.checkoutEmailUpdate(payload); - if (opts.revalidateCheckoutPathOnSuccess && result.ok) { + if (isMarketplaceEnabled) { + const checkoutIds = await getCheckoutIds(); + const targetCheckoutIds = checkoutIds.length + ? checkoutIds + : [payload.checkout.id]; + const results = await Promise.all( + targetCheckoutIds.map((checkoutId) => + checkoutService.checkoutEmailUpdate({ + ...payload, + checkout: { + ...payload.checkout, + id: checkoutId, + }, + }), + ), + ); + const failedResult = results.find((result) => !result.ok); + + if (failedResult && !failedResult.ok) { + return failedResult; + } + } else { + const result = await checkoutService.checkoutEmailUpdate(payload); + + if (!result.ok) { + return result; + } + } + + if (opts.revalidateCheckoutPathOnSuccess) { revalidatePath(paths.checkout.asPath()); } - return result; + return ok({ success: true }); }; diff --git a/packages/features/src/cart/shared/components/cart-details.tsx b/packages/features/src/cart/shared/components/cart-details.tsx index 31caa527..0a370bad 100644 --- a/packages/features/src/cart/shared/components/cart-details.tsx +++ b/packages/features/src/cart/shared/components/cart-details.tsx @@ -15,6 +15,7 @@ import { cn } from "@nimara/ui/lib/utils"; export interface CartDetailsProps { cart: Cart; + lineCheckoutIdMap?: Record; onCartUpdate: (cartId: string) => Promise; onLineDelete: (params: { cartId: string; @@ -35,6 +36,7 @@ export interface CartDetailsProps { export const CartDetails = ({ cart, user, + lineCheckoutIdMap, onLineQuantityChange, onLineDelete, onCartUpdate, @@ -57,18 +59,21 @@ export const CartDetails = ({ ].length; const isDisabled = isProcessing || !isCartValid; + const resolveCheckoutIdForLine = (lineId: string): string => + lineCheckoutIdMap?.[lineId] ?? cart.id; const handleLineQuantityChange = async (lineId: string, quantity: number) => { setIsProcessing(true); + const checkoutId = resolveCheckoutIdForLine(lineId); const result = await onLineQuantityChange({ - cartId: cart.id, + cartId: checkoutId, lineId, quantity, }); if (result.ok) { - await onCartUpdate(cart.id); + await onCartUpdate(checkoutId); setIsProcessing(false); return; @@ -84,14 +89,15 @@ export const CartDetails = ({ const handleLineDelete = async (lineId: string) => { setIsProcessing(true); + const checkoutId = resolveCheckoutIdForLine(lineId); const result = await onLineDelete({ - cartId: cart.id, + cartId: checkoutId, lineId, }); if (result.ok) { - await onCartUpdate(cart.id); + await onCartUpdate(checkoutId); router.refresh(); } else { result.errors.forEach((error) => { diff --git a/packages/features/src/product-detail-page/shared/actions/add-to-bag.core.ts b/packages/features/src/product-detail-page/shared/actions/add-to-bag.core.ts index ca2e5c62..8efa59f4 100644 --- a/packages/features/src/product-detail-page/shared/actions/add-to-bag.core.ts +++ b/packages/features/src/product-detail-page/shared/actions/add-to-bag.core.ts @@ -1,4 +1,4 @@ -import { type AsyncResult, err } from "@nimara/domain/objects/Result"; +import { type AsyncResult } from "@nimara/domain/objects/Result"; import { type User } from "@nimara/domain/objects/User"; import type { Region } from "@nimara/foundation/regions/types"; import type { ServiceRegistry } from "@nimara/infrastructure/types"; @@ -9,7 +9,6 @@ export type AddToBagCtx = { cart: number; }; cartId: string | null; - marketplaceEnabled: boolean; region: Region; }; @@ -35,48 +34,11 @@ export async function addToBag( input: AddToBagInput, ctx: AddToBagCtx, ): Promise { - const { variantId, quantity = 1, clientProductVendorId = null } = input; - const { region, cartId, accessToken, cacheTTL, marketplaceEnabled } = ctx; + const { variantId, quantity = 1 } = input; + const { region, cartId, accessToken, cacheTTL } = ctx; const cartService = await services.getCartService(); - if (marketplaceEnabled && cartId) { - const cartResult = await cartService.cartGet({ - cartId, - countryCode: region.market.countryCode, - languageCode: region.language.code, - options: { - next: { - revalidate: 0, - tags: [`CHECKOUT:${cartId}`], - }, - }, - }); - - if (cartResult.ok && cartResult.data.lines.length > 0) { - const cartVendorId = - cartResult.data.lines - .map((line) => line.product.vendorId) - .find((vendorId): vendorId is string => vendorId != null) ?? null; - - const vendorMix = - (cartVendorId !== null && clientProductVendorId === null) || - (cartVendorId === null && clientProductVendorId !== null); - - if (vendorMix) { - return err([{ code: "VENDOR_MIX_NOT_ALLOWED_ERROR" }]); - } - - if ( - cartVendorId !== null && - clientProductVendorId !== null && - cartVendorId !== clientProductVendorId - ) { - return err([{ code: "VENDOR_MISMATCH_ERROR" }]); - } - } - } - // Get user if access token is available let userData: User | null = null; diff --git a/packages/features/src/product-detail-page/shared/components/add-to-bag.tsx b/packages/features/src/product-detail-page/shared/components/add-to-bag.tsx index d9bbae75..d84725db 100644 --- a/packages/features/src/product-detail-page/shared/components/add-to-bag.tsx +++ b/packages/features/src/product-detail-page/shared/components/add-to-bag.tsx @@ -5,18 +5,9 @@ import { useTranslations } from "next-intl"; import { useCallback, useState } from "react"; import { type BaseError } from "@nimara/domain/objects/Error"; -import { type Cart } from "@nimara/domain/objects/Cart"; import { LocalizedLink } from "@nimara/i18n/routing"; import { type MessagePath } from "@nimara/i18n/types"; import { Button } from "@nimara/ui/components/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@nimara/ui/components/dialog"; import { ToastAction } from "@nimara/ui/components/toast"; import { useToast } from "@nimara/ui/hooks"; @@ -24,7 +15,6 @@ import { type AddToBagAction } from "../types"; type AddToBagProps = { addToBagAction: AddToBagAction; - cart: Cart | null; cartPath: string; isVariantAvailable: boolean; productVendorId: string | null; @@ -35,39 +25,14 @@ export const AddToBag = ({ variantId, isVariantAvailable, productVendorId, - cart, cartPath, addToBagAction, }: AddToBagProps) => { const t = useTranslations(); const { toast } = useToast(); const [isProcessing, setIsProcessing] = useState(false); - const [showVendorMismatchModal, setShowVendorMismatchModal] = useState(false); - - const marketplaceEnabled = - process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; const handleProductAdd = async () => { - if (marketplaceEnabled && cart?.lines?.length) { - const cartVendorId = - cart.lines - .map((line) => line.product.vendorId) - .find((vendorId): vendorId is string => vendorId != null) ?? null; - - const vendorMix = - (cartVendorId !== null && productVendorId === null) || - (cartVendorId === null && productVendorId !== null); - const vendorMismatch = - cartVendorId !== null && - productVendorId !== null && - cartVendorId !== productVendorId; - - if (vendorMix || vendorMismatch) { - setShowVendorMismatchModal(true); - return; - } - } - setIsProcessing(true); const resultLinesAdd = await addToBagAction({ @@ -75,23 +40,7 @@ export const AddToBag = ({ variantId, }); - const vendorErrorCodes = [ - "VENDOR_MISMATCH_ERROR", - "VENDOR_MIX_NOT_ALLOWED_ERROR", - ]; - if (!resultLinesAdd.ok) { - if ( - marketplaceEnabled && - resultLinesAdd.errors.some((error) => - vendorErrorCodes.includes(error.code), - ) - ) { - setShowVendorMismatchModal(true); - setIsProcessing(false); - return; - } - resultLinesAdd.errors.forEach((error: BaseError) => { if (error.field) { toast({ @@ -130,46 +79,20 @@ export const AddToBag = ({ }, []); return ( - <> - - - {marketplaceEnabled && ( - - - - {t("cart.vendor-restriction-title")} - - -
-

{t("cart.vendor-restriction-description")}

-

{t("cart.vendor-restriction-temporary")}

-
-
- - - -
-
+ ); }; diff --git a/packages/features/src/product-detail-page/shared/components/variant-selector.tsx b/packages/features/src/product-detail-page/shared/components/variant-selector.tsx index 5105c9cd..c6e3c0de 100644 --- a/packages/features/src/product-detail-page/shared/components/variant-selector.tsx +++ b/packages/features/src/product-detail-page/shared/components/variant-selector.tsx @@ -179,7 +179,6 @@ export const VariantSelector = ({
{ - invariant(serviceState.transactionId, "Missing transaction id."); invariant(serviceState.clientSDK, "Client not initiated."); const isUsingNewPaymentMethod = !paymentSecret; @@ -38,10 +37,12 @@ export const paymentExecuteInfra = const returnUrl = new URL(redirectUrl); - returnUrl.searchParams.append( - QUERY_PARAMS.TRANSACTION_ID, - serviceState.transactionId, - ); + if (serviceState.transactionId) { + returnUrl.searchParams.append( + QUERY_PARAMS.TRANSACTION_ID, + serviceState.transactionId, + ); + } // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const { error } = await serviceState.clientSDK.confirmPayment({ @@ -73,7 +74,7 @@ export const paymentExecuteInfra = if (error) { logger.error("Payment execution failed.", { - transactionId: serviceState.transactionId, + transactionId: serviceState.transactionId ?? null, paymentSecret, redirectUrl, originalError: { From 703dd3e5bc063ccdcae337ef4c81f7583b7da57e Mon Sep 17 00:00:00 2001 From: Tomasz Stuba Date: Fri, 27 Mar 2026 10:11:21 +0100 Subject: [PATCH 09/23] feat: add refund logic --- .../src/app/api/saleor/manifest/route.ts | 32 ++++ .../route.test.ts | 172 ++++++++++++++++++ .../transaction-refund-requested/route.ts | 163 +++++++++++++++++ .../src/lib/saleor/webhook-signature.ts | 125 +++++++++++++ 4 files changed, 492 insertions(+) create mode 100644 apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.test.ts create mode 100644 apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.ts create mode 100644 apps/marketplace/src/lib/saleor/webhook-signature.ts diff --git a/apps/marketplace/src/app/api/saleor/manifest/route.ts b/apps/marketplace/src/app/api/saleor/manifest/route.ts index 7ddf1f31..a04b5f4a 100644 --- a/apps/marketplace/src/app/api/saleor/manifest/route.ts +++ b/apps/marketplace/src/app/api/saleor/manifest/route.ts @@ -110,6 +110,38 @@ export async function GET(request: NextRequest) { }`, syncEvents: [], }, + { + name: "Transaction refund requested", + targetUrl: manifestUrl( + baseUrl, + "/api/saleor/webhooks/transaction-refund-requested", + ), + asyncEvents: [], + syncEvents: ["TRANSACTION_REFUND_REQUESTED"], + query: `subscription TransactionRefundRequestedSubscription { + event { + ... on TransactionRefundRequested { + action { + actionType + amount + } + transaction { + id + pspReference + sourceObject { + ... on Order { + total { + gross { + currency + } + } + } + } + } + } + } +}`, + }, ], brand: { logo: { diff --git a/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.test.ts b/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.test.ts new file mode 100644 index 00000000..028d173c --- /dev/null +++ b/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.test.ts @@ -0,0 +1,172 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { POST } from "./route"; + +const { + createRefundMock, + getStripeClientMock, + verifySaleorWebhookSignatureMock, +} = vi.hoisted(() => ({ + createRefundMock: vi.fn(), + getStripeClientMock: vi.fn(), + verifySaleorWebhookSignatureMock: vi.fn(), +})); + +vi.mock("@/lib/saleor/webhook-signature", () => ({ + verifySaleorWebhookSignature: verifySaleorWebhookSignatureMock, +})); + +vi.mock("@/lib/stripe/client", () => ({ + getStripeClient: getStripeClientMock, +})); + +function createWebhookRequest(payload: unknown): Request { + return new Request( + "http://localhost/api/saleor/webhooks/transaction-refund-requested", + { + body: JSON.stringify(payload), + headers: { + "content-type": "application/json", + "saleor-api-url": "https://example.saleor.cloud/graphql/", + "saleor-domain": "example.saleor.cloud", + "saleor-event": "TRANSACTION_REFUND_REQUESTED", + "saleor-signature": "signature", + }, + method: "POST", + }, + ); +} + +const refundEventPayload = { + action: { + actionType: "REFUND", + amount: 12.34, + }, + transaction: { + id: "txn-1", + pspReference: "pi_123", + sourceObject: { + total: { + gross: { + currency: "usd", + }, + }, + }, + }, +}; + +const originalStripeSecretKey = process.env.STRIPE_SECRET_KEY; + +describe("transaction-refund-requested webhook", () => { + beforeEach(() => { + vi.clearAllMocks(); + + process.env.STRIPE_SECRET_KEY = "sk_test_123"; + + verifySaleorWebhookSignatureMock.mockResolvedValue({ success: true }); + getStripeClientMock.mockReturnValue({ + refunds: { + create: createRefundMock, + }, + }); + createRefundMock.mockResolvedValue({ + amount: 1234, + currency: "usd", + failure_reason: null, + id: "re_1", + status: "succeeded", + }); + }); + + afterEach(() => { + process.env.STRIPE_SECRET_KEY = originalStripeSecretKey; + }); + + it("returns REFUND_SUCCESS for succeeded Stripe refund", async () => { + const response = await POST(createWebhookRequest(refundEventPayload) as never); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(createRefundMock).toHaveBeenCalledWith({ + payment_intent: "pi_123", + amount: 1234, + idempotencyKey: "refund-txn-1-1234", + metadata: { + saleor_transaction_id: "txn-1", + }, + }); + expect(data).toEqual({ + amount: "12.34", + externalUrl: "https://dashboard.stripe.com/test/refunds/re_1", + message: null, + pspReference: "re_1", + result: "REFUND_SUCCESS", + }); + }); + + it("returns 400 when Saleor webhook signature is invalid", async () => { + verifySaleorWebhookSignatureMock.mockResolvedValue({ + success: false, + error: "Invalid Saleor webhook signature.", + details: { message: "Invalid signature" }, + }); + + const response = await POST(createWebhookRequest(refundEventPayload) as never); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data).toEqual({ + error: "Invalid Saleor webhook signature.", + details: { message: "Invalid signature" }, + }); + expect(createRefundMock).not.toHaveBeenCalled(); + }); + + it("returns 422 for invalid webhook payload", async () => { + const response = await POST(createWebhookRequest({ invalid: true }) as never); + const data = await response.json(); + + expect(response.status).toBe(422); + expect(data.error).toBe("Invalid Saleor refund webhook payload."); + expect(createRefundMock).not.toHaveBeenCalled(); + }); + + it("returns 500 when Stripe refund creation fails", async () => { + createRefundMock.mockRejectedValue(new Error("Stripe refund failed")); + + const response = await POST(createWebhookRequest(refundEventPayload) as never); + const data = await response.json(); + + expect(response.status).toBe(500); + expect(data).toEqual({ + error: "Failed to process Stripe refund.", + details: { message: "Stripe refund failed" }, + }); + }); + + it.each([ + { status: "pending", expected: "REFUND_REQUEST" }, + { status: "requires_action", expected: "REFUND_REQUEST" }, + { status: "failed", expected: "REFUND_FAILURE" }, + { status: "canceled", expected: "REFUND_FAILURE" }, + ])( + "maps Stripe status $status to $expected", + async ({ expected, status }) => { + createRefundMock.mockResolvedValue({ + amount: 1234, + currency: "usd", + failure_reason: null, + id: "re_status", + status, + }); + + const response = await POST( + createWebhookRequest(refundEventPayload) as never, + ); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.result).toBe(expected); + }, + ); +}); diff --git a/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.ts b/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.ts new file mode 100644 index 00000000..b901fa9e --- /dev/null +++ b/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.ts @@ -0,0 +1,163 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import { verifySaleorWebhookSignature } from "@/lib/saleor/webhook-signature"; +import { getStripeClient } from "@/lib/stripe/client"; +import { getAmountFromCents, getCentsFromAmount } from "@/lib/stripe/currency"; +import { marketplaceLogger } from "@/services/logging"; + +const refundEventSchema = z.object({ + action: z.object({ + actionType: z.literal("REFUND"), + amount: z.number().positive(), + }), + transaction: z.object({ + id: z.string().min(1), + pspReference: z.string().min(1), + sourceObject: z.object({ + total: z.object({ + gross: z.object({ + currency: z.string().length(3), + }), + }), + }), + }), +}); + +const bodySchema = z.union([ + refundEventSchema, + z.object({ + event: refundEventSchema, + }), +]); + +const getRefundDashboardUrl = ({ refundId }: { refundId: string }) => { + const prefix = process.env.STRIPE_SECRET_KEY?.includes("test") + ? "test/" + : ""; + + return `https://dashboard.stripe.com/${prefix}refunds/${refundId}`; +}; + +const mapStripeRefundStatusToSaleorResult = (status: string) => { + if (status === "succeeded") { + return "REFUND_SUCCESS"; + } + + if (status === "pending" || status === "requires_action") { + return "REFUND_REQUEST"; + } + + if (status === "failed" || status === "canceled") { + return "REFUND_FAILURE"; + } + + throw new Error(`Unsupported Stripe refund status: ${status}.`); +}; + +const responseError = ({ + error, + details, + status, +}: { + details?: unknown; + error: string; + status: number; +}) => + NextResponse.json(details ? { error, details } : { error }, { + status, + }); + +export async function POST(request: NextRequest) { + const rawPayload = await request.text(); + const verificationResult = await verifySaleorWebhookSignature({ + headers: request.headers, + payload: rawPayload, + }); + + if (!verificationResult.success) { + marketplaceLogger.warning( + "Saleor refund webhook signature verification failed.", + { + details: verificationResult.details, + }, + ); + + return responseError({ + error: verificationResult.error, + details: verificationResult.details, + status: 400, + }); + } + + let bodyUnknown: unknown; + + try { + bodyUnknown = JSON.parse(rawPayload); + } catch { + return responseError({ + error: "Invalid Saleor webhook payload.", + status: 400, + }); + } + + const bodyParsed = bodySchema.safeParse(bodyUnknown); + + if (!bodyParsed.success) { + marketplaceLogger.warning("Invalid Saleor refund webhook payload.", { + details: bodyParsed.error.flatten(), + }); + + return responseError({ + error: "Invalid Saleor refund webhook payload.", + details: bodyParsed.error.flatten(), + status: 422, + }); + } + + const event = "event" in bodyParsed.data ? bodyParsed.data.event : bodyParsed.data; + const currency = event.transaction.sourceObject.total.gross.currency.toUpperCase(); + const amountInMinorUnits = getCentsFromAmount({ + amount: event.action.amount, + currency, + }); + + const stripe = getStripeClient(); + const idempotencyKey = `refund-${event.transaction.id}-${amountInMinorUnits}`; + + try { + const refund = await stripe.refunds.create({ + payment_intent: event.transaction.pspReference, + amount: amountInMinorUnits, + idempotencyKey, + metadata: { + saleor_transaction_id: event.transaction.id, + }, + }); + + const responseBody = { + pspReference: refund.id, + result: mapStripeRefundStatusToSaleorResult(refund.status), + amount: getAmountFromCents({ + amount: refund.amount, + currency: refund.currency.toUpperCase(), + }), + message: refund.failure_reason, + externalUrl: getRefundDashboardUrl({ refundId: refund.id }), + }; + + return NextResponse.json(responseBody); + } catch (error) { + marketplaceLogger.error("Failed to process Stripe refund webhook.", { + error: error instanceof Error ? error.message : "Unknown error", + saleorTransactionId: event.transaction.id, + stripePaymentIntentId: event.transaction.pspReference, + }); + + return responseError({ + error: "Failed to process Stripe refund.", + details: error instanceof Error ? { message: error.message } : undefined, + status: 500, + }); + } +} diff --git a/apps/marketplace/src/lib/saleor/webhook-signature.ts b/apps/marketplace/src/lib/saleor/webhook-signature.ts new file mode 100644 index 00000000..1d22a145 --- /dev/null +++ b/apps/marketplace/src/lib/saleor/webhook-signature.ts @@ -0,0 +1,125 @@ +import { createRemoteJWKSet, flattenedVerify } from "jose"; +import { z } from "zod"; + +const saleorWebhookHeadersSchema = z.object({ + "saleor-api-url": z.string().url(), + "saleor-domain": z.string().min(1), + "saleor-event": z.string().min(1), + "saleor-signature": z.string().min(1), +}); + +type VerifySaleorWebhookSignatureResult = + | { + success: true; + } + | { + details?: unknown; + error: string; + success: false; + }; + +const jwksResolverCache = new Map>(); + +const detachedJwsSchema = z + .string() + .regex( + /^[A-Za-z0-9_-]+\.\.[A-Za-z0-9_-]+$/, + "Invalid detached JWS format. Expected '..'.", + ); + +const getJwksUrl = (saleorApiUrl: string) => { + const origin = new URL(saleorApiUrl).origin; + + return new URL("/.well-known/jwks.json", origin); +}; + +const getJwksResolver = (jwksUrl: URL) => { + const key = jwksUrl.href; + const cached = jwksResolverCache.get(key); + + if (cached) { + return cached; + } + + const resolver = createRemoteJWKSet(jwksUrl); + + jwksResolverCache.set(key, resolver); + + return resolver; +}; + +const parseDetachedJws = (value: string) => { + const parsed = detachedJwsSchema.safeParse(value); + + if (!parsed.success) { + return null; + } + + const [protectedHeader, signature] = parsed.data.split(".."); + + if (!protectedHeader || !signature) { + return null; + } + + return { protectedHeader, signature }; +}; + +export const verifySaleorWebhookSignature = async ({ + headers, + payload, +}: { + headers: Request["headers"]; + payload: string; +}): Promise => { + const parsedHeaders = saleorWebhookHeadersSchema.safeParse({ + "saleor-api-url": headers.get("saleor-api-url"), + "saleor-domain": headers.get("saleor-domain"), + "saleor-event": headers.get("saleor-event"), + "saleor-signature": headers.get("saleor-signature"), + }); + + if (!parsedHeaders.success) { + return { + success: false, + error: "Invalid Saleor webhook headers.", + details: parsedHeaders.error.flatten(), + }; + } + + const detachedJws = parseDetachedJws(parsedHeaders.data["saleor-signature"]); + + if (!detachedJws) { + return { + success: false, + error: "Invalid Saleor webhook signature format.", + details: { + expectedFormat: "..", + }, + }; + } + + const jwksUrl = getJwksUrl(parsedHeaders.data["saleor-api-url"]); + + try { + await flattenedVerify( + { + protected: detachedJws.protectedHeader, + payload, + signature: detachedJws.signature, + }, + getJwksResolver(jwksUrl), + ); + } catch (error) { + return { + success: false, + error: "Invalid Saleor webhook signature.", + details: { + jwksUrl: jwksUrl.href, + message: + error instanceof Error ? error.message : "Signature verification failed.", + }, + }; + } + + return { success: true }; +}; From 34c7f15dd039b8157ea8d7cbdc6b0676b0a534f6 Mon Sep 17 00:00:00 2001 From: Tomasz Stuba Date: Fri, 27 Mar 2026 15:07:15 +0100 Subject: [PATCH 10/23] fix: fix manifest --- apps/marketplace/src/app/api/saleor/manifest/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/marketplace/src/app/api/saleor/manifest/route.ts b/apps/marketplace/src/app/api/saleor/manifest/route.ts index a04b5f4a..24d8d10d 100644 --- a/apps/marketplace/src/app/api/saleor/manifest/route.ts +++ b/apps/marketplace/src/app/api/saleor/manifest/route.ts @@ -128,7 +128,7 @@ export async function GET(request: NextRequest) { transaction { id pspReference - sourceObject { + sourceObject: order { ... on Order { total { gross { From 0a13f54ec50a0c5e4c2235c2d7775b262212ee0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bogda=C5=82?= Date: Tue, 7 Apr 2026 14:03:46 +0200 Subject: [PATCH 11/23] feat: unify stripe envs --- .env.example | 3 +-- apps/marketplace/src/lib/config.ts | 6 +++--- apps/marketplace/src/lib/stripe/connect.ts | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index c082fa81..640c3d5b 100644 --- a/.env.example +++ b/.env.example @@ -70,7 +70,6 @@ MARKETPLACE_SMTP_PASSWORD= MARKETPLACE_SMTP_SECURE=false MARKETPLACE_EMAIL_FROM= MARKETPLACE_SUPERADMIN_EMAIL= -# Stripe Connect (marketplace vendor onboarding) -MARKETPLACE_STRIPE_SECRET_KEY= +# Stripe Connect (marketplace vendor onboarding; uses STRIPE_SECRET_KEY above) MARKETPLACE_STRIPE_CONNECT_WEBHOOK_SECRET= MARKETPLACE_STRIPE_CONNECT_DEFAULT_COUNTRY=US diff --git a/apps/marketplace/src/lib/config.ts b/apps/marketplace/src/lib/config.ts index 5ea64a49..9f771d11 100644 --- a/apps/marketplace/src/lib/config.ts +++ b/apps/marketplace/src/lib/config.ts @@ -82,8 +82,8 @@ const envSchema = z.object({ MARKETPLACE_EMAIL_FROM: z.string().optional(), MARKETPLACE_SUPERADMIN_EMAIL: z.string().optional(), - // Stripe Connect (marketplace vendor onboarding) - MARKETPLACE_STRIPE_SECRET_KEY: z.string().optional(), + // Stripe (shared with storefront payment flows; Connect onboarding / payouts) + STRIPE_SECRET_KEY: z.string().optional(), MARKETPLACE_STRIPE_CONNECT_WEBHOOK_SECRET: z.string().optional(), MARKETPLACE_STRIPE_CONNECT_DEFAULT_COUNTRY: z.string().default("US"), }); @@ -183,7 +183,7 @@ export const config = { superadminEmail: env.MARKETPLACE_SUPERADMIN_EMAIL, }, stripeConnect: { - secretKey: env.MARKETPLACE_STRIPE_SECRET_KEY, + secretKey: env.STRIPE_SECRET_KEY, webhookSecret: env.MARKETPLACE_STRIPE_CONNECT_WEBHOOK_SECRET, defaultCountry: env.MARKETPLACE_STRIPE_CONNECT_DEFAULT_COUNTRY, }, diff --git a/apps/marketplace/src/lib/stripe/connect.ts b/apps/marketplace/src/lib/stripe/connect.ts index 3d242a22..96056cd9 100644 --- a/apps/marketplace/src/lib/stripe/connect.ts +++ b/apps/marketplace/src/lib/stripe/connect.ts @@ -26,7 +26,7 @@ function assertStripeSecretKey(): string { const secretKey = config.stripeConnect.secretKey; if (!secretKey) { - throw new Error("MARKETPLACE_STRIPE_SECRET_KEY is not set"); + throw new Error("STRIPE_SECRET_KEY is not set"); } return secretKey; From 25cb9753cdc09dab4f044890abdc9e30d080abaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bogda=C5=82?= Date: Tue, 7 Apr 2026 14:16:53 +0200 Subject: [PATCH 12/23] Remove linter warnings and adapt types --- .../app/api/payments/payment-intent/route.ts | 8 +- .../app/api/payments/stripe/webhooks/route.ts | 4 +- .../route.test.ts | 16 +- .../transaction-refund-requested/route.ts | 10 +- .../src/graphql/generated/client.ts | 86 +- .../mutations/TransactionCreate.graphql | 2 +- .../src/lib/saleor/webhook-signature.ts | 9 +- apps/marketplace/src/services/transactions.ts | 8 +- .../saleor/graphql/fragments/generated.ts | 1408 +++++++++++++---- apps/stripe/src/graphql/generated/client.ts | 39 + packages/codegen/schema.ts | 40 + .../saleor/graphql/queries/generated.ts | 79 +- .../saleor/graphql/fragments/generated.ts | 48 +- 13 files changed, 1310 insertions(+), 447 deletions(-) diff --git a/apps/marketplace/src/app/api/payments/payment-intent/route.ts b/apps/marketplace/src/app/api/payments/payment-intent/route.ts index 39e75a35..1134ce87 100644 --- a/apps/marketplace/src/app/api/payments/payment-intent/route.ts +++ b/apps/marketplace/src/app/api/payments/payment-intent/route.ts @@ -2,7 +2,7 @@ import { createHash, randomUUID } from "crypto"; import { type NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import type { TransactionCreateVariables } from "@/graphql/generated/client"; +import type { TransactionCreateMutationVariables } from "@/graphql/generated/client"; import { getServerAuthToken } from "@/lib/auth/server"; import { getAppConfig } from "@/lib/saleor/app-config"; import { getStripeClient } from "@/lib/stripe/client"; @@ -250,8 +250,8 @@ export async function POST(request: NextRequest) { return; } - const checkoutTransactionsData = - entry.value.data as CheckoutTransactionsPayload; + const checkoutTransactionsData = entry.value + .data as CheckoutTransactionsPayload; const hasExistingTransaction = checkoutTransactionsData.checkout?.transactions?.some( (transaction) => transaction.pspReference === paymentIntent.id, @@ -281,7 +281,7 @@ export async function POST(request: NextRequest) { const transactionCreateSettled = await Promise.allSettled( checkoutsToCreate.map((checkout) => { - const transactionVariables: TransactionCreateVariables = { + const transactionVariables: TransactionCreateMutationVariables = { id: checkout.checkoutId, transaction: { name: "PaymentIntent created", diff --git a/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts b/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts index b2069bb5..bee92b4e 100644 --- a/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts +++ b/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts @@ -1,6 +1,6 @@ import { type NextRequest, NextResponse } from "next/server"; -import type { TransactionCreateVariables } from "@/graphql/generated/client"; +import type { TransactionCreateMutationVariables } from "@/graphql/generated/client"; import { getAppConfig } from "@/lib/saleor/app-config"; import { verifyStripeWebhookSignature } from "@/lib/stripe/webhook-signature"; import { checkoutService } from "@/services/checkouts"; @@ -346,7 +346,7 @@ export async function POST(request: NextRequest) { }; } - const transactionVariables: TransactionCreateVariables = { + const transactionVariables: TransactionCreateMutationVariables = { id: checkoutId, transaction: { name: "PaymentIntent Succeeded", diff --git a/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.test.ts b/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.test.ts index 028d173c..f49fd65f 100644 --- a/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.test.ts +++ b/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.test.ts @@ -83,7 +83,9 @@ describe("transaction-refund-requested webhook", () => { }); it("returns REFUND_SUCCESS for succeeded Stripe refund", async () => { - const response = await POST(createWebhookRequest(refundEventPayload) as never); + const response = await POST( + createWebhookRequest(refundEventPayload) as never, + ); const data = await response.json(); expect(response.status).toBe(200); @@ -111,7 +113,9 @@ describe("transaction-refund-requested webhook", () => { details: { message: "Invalid signature" }, }); - const response = await POST(createWebhookRequest(refundEventPayload) as never); + const response = await POST( + createWebhookRequest(refundEventPayload) as never, + ); const data = await response.json(); expect(response.status).toBe(400); @@ -123,7 +127,9 @@ describe("transaction-refund-requested webhook", () => { }); it("returns 422 for invalid webhook payload", async () => { - const response = await POST(createWebhookRequest({ invalid: true }) as never); + const response = await POST( + createWebhookRequest({ invalid: true }) as never, + ); const data = await response.json(); expect(response.status).toBe(422); @@ -134,7 +140,9 @@ describe("transaction-refund-requested webhook", () => { it("returns 500 when Stripe refund creation fails", async () => { createRefundMock.mockRejectedValue(new Error("Stripe refund failed")); - const response = await POST(createWebhookRequest(refundEventPayload) as never); + const response = await POST( + createWebhookRequest(refundEventPayload) as never, + ); const data = await response.json(); expect(response.status).toBe(500); diff --git a/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.ts b/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.ts index b901fa9e..a6870d43 100644 --- a/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.ts +++ b/apps/marketplace/src/app/api/saleor/webhooks/transaction-refund-requested/route.ts @@ -32,9 +32,7 @@ const bodySchema = z.union([ ]); const getRefundDashboardUrl = ({ refundId }: { refundId: string }) => { - const prefix = process.env.STRIPE_SECRET_KEY?.includes("test") - ? "test/" - : ""; + const prefix = process.env.STRIPE_SECRET_KEY?.includes("test") ? "test/" : ""; return `https://dashboard.stripe.com/${prefix}refunds/${refundId}`; }; @@ -115,8 +113,10 @@ export async function POST(request: NextRequest) { }); } - const event = "event" in bodyParsed.data ? bodyParsed.data.event : bodyParsed.data; - const currency = event.transaction.sourceObject.total.gross.currency.toUpperCase(); + const event = + "event" in bodyParsed.data ? bodyParsed.data.event : bodyParsed.data; + const currency = + event.transaction.sourceObject.total.gross.currency.toUpperCase(); const amountInMinorUnits = getCentsFromAmount({ amount: event.action.amount, currency, diff --git a/apps/marketplace/src/graphql/generated/client.ts b/apps/marketplace/src/graphql/generated/client.ts index 92d9edba..a6daaba4 100644 --- a/apps/marketplace/src/graphql/generated/client.ts +++ b/apps/marketplace/src/graphql/generated/client.ts @@ -22756,6 +22756,30 @@ export type ProductVariantDeletedProductVariantArgs = { channel?: InputMaybe; }; +/** + * Event sent when product variant discounted price is recalculated. + * + * Added in Saleor 3.22. + */ +export type ProductVariantDiscountedPriceUpdated = Event & { + /** The channel where the price changed. */ + channel: Channel; + /** Time of the event. */ + issuedAt: Maybe; + /** The user or application that triggered the event. */ + issuingPrincipal: Maybe; + /** The new discounted price. */ + newPrice: Money; + /** The previous discounted price. */ + previousPrice: Money; + /** The product variant the event relates to. */ + productVariant: ProductVariant; + /** The application receiving the webhook. */ + recipient: Maybe; + /** Saleor version that triggered the event. */ + version: Maybe; +}; + export type ProductVariantFilterInput = { isPreorder?: InputMaybe; metadata?: InputMaybe>; @@ -27901,6 +27925,14 @@ export type Subscription = { * Note: this API is currently in Feature Preview and can be subject to changes at later point. */ orderUpdated: Maybe; + /** + * Event sent when product variant discounted price is recalculated. + * + * Added in Saleor 3.22. + * + * Note: this API is currently in Feature Preview and can be subject to changes at later point. + */ + productVariantDiscountedPriceUpdated: Maybe; }; @@ -28003,6 +28035,11 @@ export type SubscriptionOrderUpdatedArgs = { channels?: InputMaybe>; }; + +export type SubscriptionProductVariantDiscountedPriceUpdatedArgs = { + channels?: InputMaybe>; +}; + export type TaxCalculationStrategy = | 'FLAT_RATES' | 'TAX_APP'; @@ -31323,6 +31360,7 @@ export type WebhookEventTypeAsyncEnum = | 'PRODUCT_VARIANT_CREATED' /** A product variant is deleted. Warning: this event will not be executed when parent product has been deleted. Check PRODUCT_DELETED. */ | 'PRODUCT_VARIANT_DELETED' + | 'PRODUCT_VARIANT_DISCOUNTED_PRICE_UPDATED' /** A product variant metadata is updated. */ | 'PRODUCT_VARIANT_METADATA_UPDATED' /** A product variant is out of stock. */ @@ -31648,6 +31686,7 @@ export type WebhookEventTypeEnum = | 'PRODUCT_VARIANT_CREATED' /** A product variant is deleted. Warning: this event will not be executed when parent product has been deleted. Check PRODUCT_DELETED. */ | 'PRODUCT_VARIANT_DELETED' + | 'PRODUCT_VARIANT_DISCOUNTED_PRICE_UPDATED' /** A product variant metadata is updated. */ | 'PRODUCT_VARIANT_METADATA_UPDATED' /** A product variant is out of stock. */ @@ -31889,6 +31928,7 @@ export type WebhookSampleEventTypeEnum = | 'PRODUCT_VARIANT_BACK_IN_STOCK' | 'PRODUCT_VARIANT_CREATED' | 'PRODUCT_VARIANT_DELETED' + | 'PRODUCT_VARIANT_DISCOUNTED_PRICE_UPDATED' | 'PRODUCT_VARIANT_METADATA_UPDATED' | 'PRODUCT_VARIANT_OUT_OF_STOCK' | 'PRODUCT_VARIANT_STOCK_UPDATED' @@ -32769,23 +32809,23 @@ export type ProductVariantUpdateMutationVariables = Exact<{ export type ProductVariantUpdateMutation = ProductVariantUpdateMutation_Mutation; -export type TransactionCreate_transactionCreate_TransactionCreate_transaction_TransactionItem = { id: string, name: string }; +export type TransactionCreateMutation_transactionCreate_TransactionCreate_transaction_TransactionItem = { id: string, name: string }; -export type TransactionCreate_transactionCreate_TransactionCreate_errors_TransactionCreateError = { field: string | null, message: string | null, code: TransactionCreateErrorCode }; +export type TransactionCreateMutation_transactionCreate_TransactionCreate_errors_TransactionCreateError = { field: string | null, message: string | null, code: TransactionCreateErrorCode }; -export type TransactionCreate_transactionCreate_TransactionCreate = { transaction: TransactionCreate_transactionCreate_TransactionCreate_transaction_TransactionItem | null, errors: Array }; +export type TransactionCreateMutation_transactionCreate_TransactionCreate = { transaction: TransactionCreateMutation_transactionCreate_TransactionCreate_transaction_TransactionItem | null, errors: Array }; -export type TransactionCreate_Mutation = { transactionCreate: TransactionCreate_transactionCreate_TransactionCreate | null }; +export type TransactionCreateMutation_Mutation = { transactionCreate: TransactionCreateMutation_transactionCreate_TransactionCreate | null }; -export type TransactionCreateVariables = Exact<{ +export type TransactionCreateMutationVariables = Exact<{ id: Scalars['ID']['input']; transaction: TransactionCreateInput; transactionEvent?: InputMaybe; }>; -export type TransactionCreate = TransactionCreate_Mutation; +export type TransactionCreateMutation = TransactionCreateMutation_Mutation; export type VendorCollectionCreate_collectionCreate_CollectionCreate_collection_Collection = { id: string, name: string, slug: string }; @@ -33308,7 +33348,19 @@ export type ProductDetail_product_Product_assignedAttributes_AssignedTextAttribu & { __typename: 'AssignedTextAttribute' } ); -export type ProductDetail_product_Product_assignedAttributes = ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute | ProductDetail_product_Product_assignedAttributes_AssignedDateAttribute | ProductDetail_product_Product_assignedAttributes_AssignedDateTimeAttribute | ProductDetail_product_Product_assignedAttributes_AssignedFileAttribute | ProductDetail_product_Product_assignedAttributes_EP9HcmmFCQqdoG7811YxTX6aNOeQ5zZmEGxiLqdpAO4 | ProductDetail_product_Product_assignedAttributes_AssignedMultiChoiceAttribute | ProductDetail_product_Product_assignedAttributes_AssignedNumericAttribute | ProductDetail_product_Product_assignedAttributes_AssignedPlainTextAttribute | ProductDetail_product_Product_assignedAttributes_AssignedSingleChoiceAttribute | ProductDetail_product_Product_assignedAttributes_AssignedSwatchAttribute | ProductDetail_product_Product_assignedAttributes_AssignedTextAttribute; +export type ProductDetail_product_Product_assignedAttributes = + | ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute + | ProductDetail_product_Product_assignedAttributes_AssignedDateAttribute + | ProductDetail_product_Product_assignedAttributes_AssignedDateTimeAttribute + | ProductDetail_product_Product_assignedAttributes_AssignedFileAttribute + | ProductDetail_product_Product_assignedAttributes_EP9HcmmFCQqdoG7811YxTX6aNOeQ5zZmEGxiLqdpAO4 + | ProductDetail_product_Product_assignedAttributes_AssignedMultiChoiceAttribute + | ProductDetail_product_Product_assignedAttributes_AssignedNumericAttribute + | ProductDetail_product_Product_assignedAttributes_AssignedPlainTextAttribute + | ProductDetail_product_Product_assignedAttributes_AssignedSingleChoiceAttribute + | ProductDetail_product_Product_assignedAttributes_AssignedSwatchAttribute + | ProductDetail_product_Product_assignedAttributes_AssignedTextAttribute +; export type ProductDetail_product_Product_pricing_ProductPricingInfo_priceRange_TaxedMoneyRange_start_TaxedMoney_gross_Money = { amount: number, currency: string }; @@ -33510,7 +33562,19 @@ export type ProductVariantDetail_productVariant_ProductVariant_assignedAttribute & { __typename: 'AssignedTextAttribute' } ); -export type ProductVariantDetail_productVariant_ProductVariant_assignedAttributes = ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedDateAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedDateTimeAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedFileAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_EP9HcmmFCQqdoG7811YxTX6aNOeQ5zZmEGxiLqdpAO4 | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedMultiChoiceAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedNumericAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedPlainTextAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedSingleChoiceAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedSwatchAttribute | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedTextAttribute; +export type ProductVariantDetail_productVariant_ProductVariant_assignedAttributes = + | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute + | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedDateAttribute + | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedDateTimeAttribute + | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedFileAttribute + | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_EP9HcmmFCQqdoG7811YxTX6aNOeQ5zZmEGxiLqdpAO4 + | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedMultiChoiceAttribute + | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedNumericAttribute + | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedPlainTextAttribute + | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedSingleChoiceAttribute + | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedSwatchAttribute + | ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedTextAttribute +; export type ProductVariantDetail_productVariant_ProductVariant_stocks_Stock_warehouse_Warehouse = { id: string, name: string }; @@ -34506,8 +34570,8 @@ export const ProductVariantUpdateMutationDocument = new TypedDocumentString(` } } `) as unknown as TypedDocumentString; -export const TransactionCreateDocument = new TypedDocumentString(` - mutation TransactionCreate($id: ID!, $transaction: TransactionCreateInput!, $transactionEvent: TransactionEventInput) { +export const TransactionCreateMutationDocument = new TypedDocumentString(` + mutation TransactionCreateMutation($id: ID!, $transaction: TransactionCreateInput!, $transactionEvent: TransactionEventInput) { transactionCreate( id: $id transaction: $transaction @@ -34524,7 +34588,7 @@ export const TransactionCreateDocument = new TypedDocumentString(` } } } - `) as unknown as TypedDocumentString; + `) as unknown as TypedDocumentString; export const VendorCollectionCreateDocument = new TypedDocumentString(` mutation VendorCollectionCreate($input: CollectionCreateInput!) { collectionCreate(input: $input) { diff --git a/apps/marketplace/src/graphql/mutations/TransactionCreate.graphql b/apps/marketplace/src/graphql/mutations/TransactionCreate.graphql index 07e2e7fc..5214e37d 100644 --- a/apps/marketplace/src/graphql/mutations/TransactionCreate.graphql +++ b/apps/marketplace/src/graphql/mutations/TransactionCreate.graphql @@ -1,4 +1,4 @@ -mutation TransactionCreate( +mutation TransactionCreateMutation( $id: ID! $transaction: TransactionCreateInput! $transactionEvent: TransactionEventInput diff --git a/apps/marketplace/src/lib/saleor/webhook-signature.ts b/apps/marketplace/src/lib/saleor/webhook-signature.ts index 1d22a145..74dfba79 100644 --- a/apps/marketplace/src/lib/saleor/webhook-signature.ts +++ b/apps/marketplace/src/lib/saleor/webhook-signature.ts @@ -18,7 +18,10 @@ type VerifySaleorWebhookSignatureResult = success: false; }; -const jwksResolverCache = new Map>(); +const jwksResolverCache = new Map< + string, + ReturnType +>(); const detachedJwsSchema = z .string() @@ -116,7 +119,9 @@ export const verifySaleorWebhookSignature = async ({ details: { jwksUrl: jwksUrl.href, message: - error instanceof Error ? error.message : "Signature verification failed.", + error instanceof Error + ? error.message + : "Signature verification failed.", }, }; } diff --git a/apps/marketplace/src/services/transactions.ts b/apps/marketplace/src/services/transactions.ts index e7531416..95713cc6 100644 --- a/apps/marketplace/src/services/transactions.ts +++ b/apps/marketplace/src/services/transactions.ts @@ -1,8 +1,8 @@ import { CheckoutTransactionsDocument, type CheckoutTransactionsVariables, - TransactionCreateDocument, - type TransactionCreateVariables, + TransactionCreateMutationDocument, + type TransactionCreateMutationVariables, } from "@/graphql/generated/client"; import { executeGraphQL } from "@/lib/graphql/execute"; @@ -20,11 +20,11 @@ class TransactionsService { } async createTransaction( - variables?: TransactionCreateVariables, + variables?: TransactionCreateMutationVariables, token?: string | null, ) { return executeGraphQL( - TransactionCreateDocument, + TransactionCreateMutationDocument, "CreateTransactionMutation", variables, token, diff --git a/apps/storefront/src/infrastructure/webhook/saleor/graphql/fragments/generated.ts b/apps/storefront/src/infrastructure/webhook/saleor/graphql/fragments/generated.ts index f4403b2c..5d80dfca 100644 --- a/apps/storefront/src/infrastructure/webhook/saleor/graphql/fragments/generated.ts +++ b/apps/storefront/src/infrastructure/webhook/saleor/graphql/fragments/generated.ts @@ -1,35 +1,219 @@ -import type * as Types from '@nimara/codegen/schema'; - -import type { DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type CollectionEventSubscriptionFragment_CollectionDeleted_collection_Collection = { slug: string }; - -export type CollectionEventSubscriptionFragment_CollectionUpdated_collection_Collection = { slug: string }; - -export type CollectionEventSubscriptionFragment_Uchm3Qz7YjEsQhTMfPIk01DEzLiWluHMnX4k1L6Dt0s = { __typename: 'AccountChangeEmailRequested' | 'AccountConfirmationRequested' | 'AccountConfirmed' | 'AccountDeleteRequested' | 'AccountDeleted' | 'AccountEmailChanged' | 'AccountSetPasswordRequested' | 'AddressCreated' | 'AddressDeleted' | 'AddressUpdated' | 'AppDeleted' | 'AppInstalled' | 'AppStatusChanged' | 'AppUpdated' | 'AttributeCreated' | 'AttributeDeleted' | 'AttributeUpdated' | 'AttributeValueCreated' | 'AttributeValueDeleted' | 'AttributeValueUpdated' }; - -export type CollectionEventSubscriptionFragment_RrvYhheqNjg9coPa7MxzKvNbxTiUdNyksBoKzspcyo = { __typename: 'CalculateTaxes' | 'CategoryCreated' | 'CategoryDeleted' | 'CategoryUpdated' | 'ChannelCreated' | 'ChannelDeleted' | 'ChannelMetadataUpdated' | 'ChannelStatusChanged' | 'ChannelUpdated' | 'CheckoutCreated' | 'CheckoutFilterShippingMethods' | 'CheckoutFullyAuthorized' | 'CheckoutFullyPaid' | 'CheckoutMetadataUpdated' | 'CheckoutUpdated' | 'CollectionCreated' | 'CollectionMetadataUpdated' | 'CustomerCreated' | 'CustomerMetadataUpdated' | 'CustomerUpdated' }; - -export type CollectionEventSubscriptionFragment_IOmIHgezj4BqSe0qBa27Ry4w4In3hD62xLNv1Dlw = { __typename: 'DraftOrderCreated' | 'DraftOrderDeleted' | 'DraftOrderUpdated' | 'FulfillmentApproved' | 'FulfillmentCanceled' | 'FulfillmentCreated' | 'FulfillmentMetadataUpdated' | 'FulfillmentTrackingNumberUpdated' | 'GiftCardCreated' | 'GiftCardDeleted' | 'GiftCardExportCompleted' | 'GiftCardMetadataUpdated' | 'GiftCardSent' | 'GiftCardStatusChanged' | 'GiftCardUpdated' | 'InvoiceDeleted' | 'InvoiceRequested' | 'InvoiceSent' | 'ListStoredPaymentMethods' | 'MenuCreated' }; - -export type CollectionEventSubscriptionFragment_NzAgx5ipNwNrprvvHc2qLsxAIqOZxfb0Ab5nlcQwU = { __typename: 'MenuDeleted' | 'MenuItemCreated' | 'MenuItemDeleted' | 'MenuItemUpdated' | 'MenuUpdated' | 'OrderBulkCreated' | 'OrderCancelled' | 'OrderConfirmed' | 'OrderCreated' | 'OrderExpired' | 'OrderFilterShippingMethods' | 'OrderFulfilled' | 'OrderFullyPaid' | 'OrderFullyRefunded' | 'OrderMetadataUpdated' | 'OrderPaid' | 'OrderRefunded' | 'OrderUpdated' | 'PageCreated' | 'PageDeleted' }; - -export type CollectionEventSubscriptionFragment_1E3eTt7xP6B7Mkgmqq5X7sVIf5QtQseWfIxUecQwhV0 = { __typename: 'PageTypeCreated' | 'PageTypeDeleted' | 'PageTypeUpdated' | 'PageUpdated' | 'PaymentAuthorize' | 'PaymentCaptureEvent' | 'PaymentConfirmEvent' | 'PaymentGatewayInitializeSession' | 'PaymentGatewayInitializeTokenizationSession' | 'PaymentListGateways' | 'PaymentMethodInitializeTokenizationSession' | 'PaymentMethodProcessTokenizationSession' | 'PaymentProcessEvent' | 'PaymentRefundEvent' | 'PaymentVoidEvent' | 'PermissionGroupCreated' | 'PermissionGroupDeleted' | 'PermissionGroupUpdated' | 'ProductCreated' | 'ProductDeleted' }; - -export type CollectionEventSubscriptionFragment_Z7rwgw8zu01Bnc5SgQqNwv0Lyp1jQ30oJz3Z8uvrhEw = { __typename: 'ProductExportCompleted' | 'ProductMediaCreated' | 'ProductMediaDeleted' | 'ProductMediaUpdated' | 'ProductMetadataUpdated' | 'ProductUpdated' | 'ProductVariantBackInStock' | 'ProductVariantCreated' | 'ProductVariantDeleted' | 'ProductVariantMetadataUpdated' | 'ProductVariantOutOfStock' | 'ProductVariantStockUpdated' | 'ProductVariantUpdated' | 'PromotionCreated' | 'PromotionDeleted' | 'PromotionEnded' | 'PromotionRuleCreated' | 'PromotionRuleDeleted' | 'PromotionRuleUpdated' | 'PromotionStarted' }; - -export type CollectionEventSubscriptionFragment_FsCnoBef8jWxBeWLo55z4FkYvj19c9pK9ySq77aoQq = { __typename: 'PromotionUpdated' | 'SaleCreated' | 'SaleDeleted' | 'SaleToggle' | 'SaleUpdated' | 'ShippingListMethodsForCheckout' | 'ShippingPriceCreated' | 'ShippingPriceDeleted' | 'ShippingPriceUpdated' | 'ShippingZoneCreated' | 'ShippingZoneDeleted' | 'ShippingZoneMetadataUpdated' | 'ShippingZoneUpdated' | 'ShopMetadataUpdated' | 'StaffCreated' | 'StaffDeleted' | 'StaffSetPasswordRequested' | 'StaffUpdated' | 'StoredPaymentMethodDeleteRequested' | 'ThumbnailCreated' }; - -export type CollectionEventSubscriptionFragment_V8I7ofYZlQ6mRbyZppdZBtAVuVhCxrv4cl7r3Nc1xQ = { __typename: 'TransactionCancelationRequested' | 'TransactionChargeRequested' | 'TransactionInitializeSession' | 'TransactionItemMetadataUpdated' | 'TransactionProcessSession' | 'TransactionRefundRequested' | 'TranslationCreated' | 'TranslationUpdated' | 'VoucherCodeExportCompleted' | 'VoucherCodesCreated' | 'VoucherCodesDeleted' | 'VoucherCreated' | 'VoucherDeleted' | 'VoucherMetadataUpdated' | 'VoucherUpdated' | 'WarehouseCreated' | 'WarehouseDeleted' | 'WarehouseMetadataUpdated' | 'WarehouseUpdated' }; - -export type CollectionEventSubscriptionFragment_CollectionDeleted = ( - { collection: CollectionEventSubscriptionFragment_CollectionDeleted_collection_Collection | null } - & { __typename: 'CollectionDeleted' } -); - -export type CollectionEventSubscriptionFragment_CollectionUpdated = ( - { collection: CollectionEventSubscriptionFragment_CollectionUpdated_collection_Collection | null } - & { __typename: 'CollectionUpdated' } -); +import type * as Types from "@nimara/codegen/schema"; + +import type { DocumentTypeDecoration } from "@graphql-typed-document-node/core"; +export type CollectionEventSubscriptionFragment_CollectionDeleted_collection_Collection = + { slug: string }; + +export type CollectionEventSubscriptionFragment_CollectionUpdated_collection_Collection = + { slug: string }; + +export type CollectionEventSubscriptionFragment_Uchm3Qz7YjEsQhTMfPIk01DEzLiWluHMnX4k1L6Dt0s = + { + __typename: + | "AccountChangeEmailRequested" + | "AccountConfirmationRequested" + | "AccountConfirmed" + | "AccountDeleteRequested" + | "AccountDeleted" + | "AccountEmailChanged" + | "AccountSetPasswordRequested" + | "AddressCreated" + | "AddressDeleted" + | "AddressUpdated" + | "AppDeleted" + | "AppInstalled" + | "AppStatusChanged" + | "AppUpdated" + | "AttributeCreated" + | "AttributeDeleted" + | "AttributeUpdated" + | "AttributeValueCreated" + | "AttributeValueDeleted" + | "AttributeValueUpdated"; + }; + +export type CollectionEventSubscriptionFragment_RrvYhheqNjg9coPa7MxzKvNbxTiUdNyksBoKzspcyo = + { + __typename: + | "CalculateTaxes" + | "CategoryCreated" + | "CategoryDeleted" + | "CategoryUpdated" + | "ChannelCreated" + | "ChannelDeleted" + | "ChannelMetadataUpdated" + | "ChannelStatusChanged" + | "ChannelUpdated" + | "CheckoutCreated" + | "CheckoutFilterShippingMethods" + | "CheckoutFullyAuthorized" + | "CheckoutFullyPaid" + | "CheckoutMetadataUpdated" + | "CheckoutUpdated" + | "CollectionCreated" + | "CollectionMetadataUpdated" + | "CustomerCreated" + | "CustomerMetadataUpdated" + | "CustomerUpdated"; + }; + +export type CollectionEventSubscriptionFragment_IOmIHgezj4BqSe0qBa27Ry4w4In3hD62xLNv1Dlw = + { + __typename: + | "DraftOrderCreated" + | "DraftOrderDeleted" + | "DraftOrderUpdated" + | "FulfillmentApproved" + | "FulfillmentCanceled" + | "FulfillmentCreated" + | "FulfillmentMetadataUpdated" + | "FulfillmentTrackingNumberUpdated" + | "GiftCardCreated" + | "GiftCardDeleted" + | "GiftCardExportCompleted" + | "GiftCardMetadataUpdated" + | "GiftCardSent" + | "GiftCardStatusChanged" + | "GiftCardUpdated" + | "InvoiceDeleted" + | "InvoiceRequested" + | "InvoiceSent" + | "ListStoredPaymentMethods" + | "MenuCreated"; + }; + +export type CollectionEventSubscriptionFragment_NzAgx5ipNwNrprvvHc2qLsxAIqOZxfb0Ab5nlcQwU = + { + __typename: + | "MenuDeleted" + | "MenuItemCreated" + | "MenuItemDeleted" + | "MenuItemUpdated" + | "MenuUpdated" + | "OrderBulkCreated" + | "OrderCancelled" + | "OrderConfirmed" + | "OrderCreated" + | "OrderExpired" + | "OrderFilterShippingMethods" + | "OrderFulfilled" + | "OrderFullyPaid" + | "OrderFullyRefunded" + | "OrderMetadataUpdated" + | "OrderPaid" + | "OrderRefunded" + | "OrderUpdated" + | "PageCreated" + | "PageDeleted"; + }; + +export type CollectionEventSubscriptionFragment_1E3eTt7xP6B7Mkgmqq5X7sVIf5QtQseWfIxUecQwhV0 = + { + __typename: + | "PageTypeCreated" + | "PageTypeDeleted" + | "PageTypeUpdated" + | "PageUpdated" + | "PaymentAuthorize" + | "PaymentCaptureEvent" + | "PaymentConfirmEvent" + | "PaymentGatewayInitializeSession" + | "PaymentGatewayInitializeTokenizationSession" + | "PaymentListGateways" + | "PaymentMethodInitializeTokenizationSession" + | "PaymentMethodProcessTokenizationSession" + | "PaymentProcessEvent" + | "PaymentRefundEvent" + | "PaymentVoidEvent" + | "PermissionGroupCreated" + | "PermissionGroupDeleted" + | "PermissionGroupUpdated" + | "ProductCreated" + | "ProductDeleted"; + }; + +export type CollectionEventSubscriptionFragment_VaNEf27jhQMii20zJvI0z7FmZ2UaHv3dIrHb6Xvvd4 = + { + __typename: + | "ProductExportCompleted" + | "ProductMediaCreated" + | "ProductMediaDeleted" + | "ProductMediaUpdated" + | "ProductMetadataUpdated" + | "ProductUpdated" + | "ProductVariantBackInStock" + | "ProductVariantCreated" + | "ProductVariantDeleted" + | "ProductVariantDiscountedPriceUpdated" + | "ProductVariantMetadataUpdated" + | "ProductVariantOutOfStock" + | "ProductVariantStockUpdated" + | "ProductVariantUpdated" + | "PromotionCreated" + | "PromotionDeleted" + | "PromotionEnded" + | "PromotionRuleCreated" + | "PromotionRuleDeleted" + | "PromotionRuleUpdated"; + }; + +export type CollectionEventSubscriptionFragment_Md8zs0I8HeprlIm0922DbYOqrfIxezBupbi88J9Cyo = + { + __typename: + | "PromotionStarted" + | "PromotionUpdated" + | "SaleCreated" + | "SaleDeleted" + | "SaleToggle" + | "SaleUpdated" + | "ShippingListMethodsForCheckout" + | "ShippingPriceCreated" + | "ShippingPriceDeleted" + | "ShippingPriceUpdated" + | "ShippingZoneCreated" + | "ShippingZoneDeleted" + | "ShippingZoneMetadataUpdated" + | "ShippingZoneUpdated" + | "ShopMetadataUpdated" + | "StaffCreated" + | "StaffDeleted" + | "StaffSetPasswordRequested" + | "StaffUpdated" + | "StoredPaymentMethodDeleteRequested"; + }; + +export type CollectionEventSubscriptionFragment_Dp5dDbTdpxGfQktwrh3Py85k1XyXjuGj32jhqJ1ZhSs = + { + __typename: + | "ThumbnailCreated" + | "TransactionCancelationRequested" + | "TransactionChargeRequested" + | "TransactionInitializeSession" + | "TransactionItemMetadataUpdated" + | "TransactionProcessSession" + | "TransactionRefundRequested" + | "TranslationCreated" + | "TranslationUpdated" + | "VoucherCodeExportCompleted" + | "VoucherCodesCreated" + | "VoucherCodesDeleted" + | "VoucherCreated" + | "VoucherDeleted" + | "VoucherMetadataUpdated" + | "VoucherUpdated" + | "WarehouseCreated" + | "WarehouseDeleted" + | "WarehouseMetadataUpdated" + | "WarehouseUpdated"; + }; + +export type CollectionEventSubscriptionFragment_CollectionDeleted = { + collection: CollectionEventSubscriptionFragment_CollectionDeleted_collection_Collection | null; +} & { __typename: "CollectionDeleted" }; + +export type CollectionEventSubscriptionFragment_CollectionUpdated = { + collection: CollectionEventSubscriptionFragment_CollectionUpdated_collection_Collection | null; +} & { __typename: "CollectionUpdated" }; export type CollectionEventSubscriptionFragment = | CollectionEventSubscriptionFragment_Uchm3Qz7YjEsQhTMfPIk01DEzLiWluHMnX4k1L6Dt0s @@ -37,76 +221,264 @@ export type CollectionEventSubscriptionFragment = | CollectionEventSubscriptionFragment_IOmIHgezj4BqSe0qBa27Ry4w4In3hD62xLNv1Dlw | CollectionEventSubscriptionFragment_NzAgx5ipNwNrprvvHc2qLsxAIqOZxfb0Ab5nlcQwU | CollectionEventSubscriptionFragment_1E3eTt7xP6B7Mkgmqq5X7sVIf5QtQseWfIxUecQwhV0 - | CollectionEventSubscriptionFragment_Z7rwgw8zu01Bnc5SgQqNwv0Lyp1jQ30oJz3Z8uvrhEw - | CollectionEventSubscriptionFragment_FsCnoBef8jWxBeWLo55z4FkYvj19c9pK9ySq77aoQq - | CollectionEventSubscriptionFragment_V8I7ofYZlQ6mRbyZppdZBtAVuVhCxrv4cl7r3Nc1xQ + | CollectionEventSubscriptionFragment_VaNEf27jhQMii20zJvI0z7FmZ2UaHv3dIrHb6Xvvd4 + | CollectionEventSubscriptionFragment_Md8zs0I8HeprlIm0922DbYOqrfIxezBupbi88J9Cyo + | CollectionEventSubscriptionFragment_Dp5dDbTdpxGfQktwrh3Py85k1XyXjuGj32jhqJ1ZhSs | CollectionEventSubscriptionFragment_CollectionDeleted - | CollectionEventSubscriptionFragment_CollectionUpdated -; - -export type MenuEventSubscriptionFragment_MenuCreated_menu_Menu = { slug: string }; - -export type MenuEventSubscriptionFragment_MenuDeleted_menu_Menu = { slug: string }; - -export type MenuEventSubscriptionFragment_MenuItemCreated_menuItem_MenuItem_menu_Menu = { slug: string }; - -export type MenuEventSubscriptionFragment_MenuItemCreated_menuItem_MenuItem = { menu: MenuEventSubscriptionFragment_MenuItemCreated_menuItem_MenuItem_menu_Menu }; - -export type MenuEventSubscriptionFragment_MenuItemDeleted_menuItem_MenuItem_menu_Menu = { slug: string }; - -export type MenuEventSubscriptionFragment_MenuItemDeleted_menuItem_MenuItem = { menu: MenuEventSubscriptionFragment_MenuItemDeleted_menuItem_MenuItem_menu_Menu }; - -export type MenuEventSubscriptionFragment_MenuItemUpdated_menuItem_MenuItem_menu_Menu = { slug: string }; - -export type MenuEventSubscriptionFragment_MenuItemUpdated_menuItem_MenuItem = { menu: MenuEventSubscriptionFragment_MenuItemUpdated_menuItem_MenuItem_menu_Menu }; - -export type MenuEventSubscriptionFragment_MenuUpdated_menu_Menu = { slug: string }; - -export type MenuEventSubscriptionFragment_Uchm3Qz7YjEsQhTMfPIk01DEzLiWluHMnX4k1L6Dt0s = { __typename: 'AccountChangeEmailRequested' | 'AccountConfirmationRequested' | 'AccountConfirmed' | 'AccountDeleteRequested' | 'AccountDeleted' | 'AccountEmailChanged' | 'AccountSetPasswordRequested' | 'AddressCreated' | 'AddressDeleted' | 'AddressUpdated' | 'AppDeleted' | 'AppInstalled' | 'AppStatusChanged' | 'AppUpdated' | 'AttributeCreated' | 'AttributeDeleted' | 'AttributeUpdated' | 'AttributeValueCreated' | 'AttributeValueDeleted' | 'AttributeValueUpdated' }; - -export type MenuEventSubscriptionFragment_GwYHqJDwvrv2QyEq0Kya5B6RfhCy85iuAlJzAq6AdU = { __typename: 'CalculateTaxes' | 'CategoryCreated' | 'CategoryDeleted' | 'CategoryUpdated' | 'ChannelCreated' | 'ChannelDeleted' | 'ChannelMetadataUpdated' | 'ChannelStatusChanged' | 'ChannelUpdated' | 'CheckoutCreated' | 'CheckoutFilterShippingMethods' | 'CheckoutFullyAuthorized' | 'CheckoutFullyPaid' | 'CheckoutMetadataUpdated' | 'CheckoutUpdated' | 'CollectionCreated' | 'CollectionDeleted' | 'CollectionMetadataUpdated' | 'CollectionUpdated' | 'CustomerCreated' }; - -export type MenuEventSubscriptionFragment_XWulXk1GqeHNvK2Zjg39D81UqhO8ZykBvc7wuJEvA = { __typename: 'CustomerMetadataUpdated' | 'CustomerUpdated' | 'DraftOrderCreated' | 'DraftOrderDeleted' | 'DraftOrderUpdated' | 'FulfillmentApproved' | 'FulfillmentCanceled' | 'FulfillmentCreated' | 'FulfillmentMetadataUpdated' | 'FulfillmentTrackingNumberUpdated' | 'GiftCardCreated' | 'GiftCardDeleted' | 'GiftCardExportCompleted' | 'GiftCardMetadataUpdated' | 'GiftCardSent' | 'GiftCardStatusChanged' | 'GiftCardUpdated' | 'InvoiceDeleted' | 'InvoiceRequested' | 'InvoiceSent' }; - -export type MenuEventSubscriptionFragment_6SZv9znezpLhGpS69dQ1GbY5yDKa5XUxykJztqqTg6U = { __typename: 'ListStoredPaymentMethods' | 'OrderBulkCreated' | 'OrderCancelled' | 'OrderConfirmed' | 'OrderCreated' | 'OrderExpired' | 'OrderFilterShippingMethods' | 'OrderFulfilled' | 'OrderFullyPaid' | 'OrderFullyRefunded' | 'OrderMetadataUpdated' | 'OrderPaid' | 'OrderRefunded' | 'OrderUpdated' | 'PageCreated' | 'PageDeleted' | 'PageTypeCreated' | 'PageTypeDeleted' | 'PageTypeUpdated' | 'PageUpdated' }; - -export type MenuEventSubscriptionFragment_6kRlk3To6sPpW0QQr52mUNjVn2CzSdpyN0o8Cy5kQ70 = { __typename: 'PaymentAuthorize' | 'PaymentCaptureEvent' | 'PaymentConfirmEvent' | 'PaymentGatewayInitializeSession' | 'PaymentGatewayInitializeTokenizationSession' | 'PaymentListGateways' | 'PaymentMethodInitializeTokenizationSession' | 'PaymentMethodProcessTokenizationSession' | 'PaymentProcessEvent' | 'PaymentRefundEvent' | 'PaymentVoidEvent' | 'PermissionGroupCreated' | 'PermissionGroupDeleted' | 'PermissionGroupUpdated' | 'ProductCreated' | 'ProductDeleted' | 'ProductExportCompleted' | 'ProductMediaCreated' | 'ProductMediaDeleted' | 'ProductMediaUpdated' }; - -export type MenuEventSubscriptionFragment_Rhl3UnNk6nEi747MfPtEghacMvrlEq0zkU4BVjs = { __typename: 'ProductMetadataUpdated' | 'ProductUpdated' | 'ProductVariantBackInStock' | 'ProductVariantCreated' | 'ProductVariantDeleted' | 'ProductVariantMetadataUpdated' | 'ProductVariantOutOfStock' | 'ProductVariantStockUpdated' | 'ProductVariantUpdated' | 'PromotionCreated' | 'PromotionDeleted' | 'PromotionEnded' | 'PromotionRuleCreated' | 'PromotionRuleDeleted' | 'PromotionRuleUpdated' | 'PromotionStarted' | 'PromotionUpdated' | 'SaleCreated' | 'SaleDeleted' | 'SaleToggle' }; - -export type MenuEventSubscriptionFragment_W5T9u8Ze80BUkN79Oq6SmIzz96pnf5x7CafMXoKzjye = { __typename: 'SaleUpdated' | 'ShippingListMethodsForCheckout' | 'ShippingPriceCreated' | 'ShippingPriceDeleted' | 'ShippingPriceUpdated' | 'ShippingZoneCreated' | 'ShippingZoneDeleted' | 'ShippingZoneMetadataUpdated' | 'ShippingZoneUpdated' | 'ShopMetadataUpdated' | 'StaffCreated' | 'StaffDeleted' | 'StaffSetPasswordRequested' | 'StaffUpdated' | 'StoredPaymentMethodDeleteRequested' | 'ThumbnailCreated' | 'TransactionCancelationRequested' | 'TransactionChargeRequested' | 'TransactionInitializeSession' | 'TransactionItemMetadataUpdated' }; - -export type MenuEventSubscriptionFragment_YvQxrNkoOvnbjiQk7JSzAwJglAbMml79KptInbPmJ50 = { __typename: 'TransactionProcessSession' | 'TransactionRefundRequested' | 'TranslationCreated' | 'TranslationUpdated' | 'VoucherCodeExportCompleted' | 'VoucherCodesCreated' | 'VoucherCodesDeleted' | 'VoucherCreated' | 'VoucherDeleted' | 'VoucherMetadataUpdated' | 'VoucherUpdated' | 'WarehouseCreated' | 'WarehouseDeleted' | 'WarehouseMetadataUpdated' | 'WarehouseUpdated' }; - -export type MenuEventSubscriptionFragment_MenuCreated = ( - { menu: MenuEventSubscriptionFragment_MenuCreated_menu_Menu | null } - & { __typename: 'MenuCreated' } -); - -export type MenuEventSubscriptionFragment_MenuDeleted = ( - { menu: MenuEventSubscriptionFragment_MenuDeleted_menu_Menu | null } - & { __typename: 'MenuDeleted' } -); - -export type MenuEventSubscriptionFragment_MenuItemCreated = ( - { menuItem: MenuEventSubscriptionFragment_MenuItemCreated_menuItem_MenuItem | null } - & { __typename: 'MenuItemCreated' } -); - -export type MenuEventSubscriptionFragment_MenuItemDeleted = ( - { menuItem: MenuEventSubscriptionFragment_MenuItemDeleted_menuItem_MenuItem | null } - & { __typename: 'MenuItemDeleted' } -); - -export type MenuEventSubscriptionFragment_MenuItemUpdated = ( - { menuItem: MenuEventSubscriptionFragment_MenuItemUpdated_menuItem_MenuItem | null } - & { __typename: 'MenuItemUpdated' } -); - -export type MenuEventSubscriptionFragment_MenuUpdated = ( - { menu: MenuEventSubscriptionFragment_MenuUpdated_menu_Menu | null } - & { __typename: 'MenuUpdated' } -); + | CollectionEventSubscriptionFragment_CollectionUpdated; + +export type MenuEventSubscriptionFragment_MenuCreated_menu_Menu = { + slug: string; +}; + +export type MenuEventSubscriptionFragment_MenuDeleted_menu_Menu = { + slug: string; +}; + +export type MenuEventSubscriptionFragment_MenuItemCreated_menuItem_MenuItem_menu_Menu = + { slug: string }; + +export type MenuEventSubscriptionFragment_MenuItemCreated_menuItem_MenuItem = { + menu: MenuEventSubscriptionFragment_MenuItemCreated_menuItem_MenuItem_menu_Menu; +}; + +export type MenuEventSubscriptionFragment_MenuItemDeleted_menuItem_MenuItem_menu_Menu = + { slug: string }; + +export type MenuEventSubscriptionFragment_MenuItemDeleted_menuItem_MenuItem = { + menu: MenuEventSubscriptionFragment_MenuItemDeleted_menuItem_MenuItem_menu_Menu; +}; + +export type MenuEventSubscriptionFragment_MenuItemUpdated_menuItem_MenuItem_menu_Menu = + { slug: string }; + +export type MenuEventSubscriptionFragment_MenuItemUpdated_menuItem_MenuItem = { + menu: MenuEventSubscriptionFragment_MenuItemUpdated_menuItem_MenuItem_menu_Menu; +}; + +export type MenuEventSubscriptionFragment_MenuUpdated_menu_Menu = { + slug: string; +}; + +export type MenuEventSubscriptionFragment_Uchm3Qz7YjEsQhTMfPIk01DEzLiWluHMnX4k1L6Dt0s = + { + __typename: + | "AccountChangeEmailRequested" + | "AccountConfirmationRequested" + | "AccountConfirmed" + | "AccountDeleteRequested" + | "AccountDeleted" + | "AccountEmailChanged" + | "AccountSetPasswordRequested" + | "AddressCreated" + | "AddressDeleted" + | "AddressUpdated" + | "AppDeleted" + | "AppInstalled" + | "AppStatusChanged" + | "AppUpdated" + | "AttributeCreated" + | "AttributeDeleted" + | "AttributeUpdated" + | "AttributeValueCreated" + | "AttributeValueDeleted" + | "AttributeValueUpdated"; + }; + +export type MenuEventSubscriptionFragment_GwYHqJDwvrv2QyEq0Kya5B6RfhCy85iuAlJzAq6AdU = + { + __typename: + | "CalculateTaxes" + | "CategoryCreated" + | "CategoryDeleted" + | "CategoryUpdated" + | "ChannelCreated" + | "ChannelDeleted" + | "ChannelMetadataUpdated" + | "ChannelStatusChanged" + | "ChannelUpdated" + | "CheckoutCreated" + | "CheckoutFilterShippingMethods" + | "CheckoutFullyAuthorized" + | "CheckoutFullyPaid" + | "CheckoutMetadataUpdated" + | "CheckoutUpdated" + | "CollectionCreated" + | "CollectionDeleted" + | "CollectionMetadataUpdated" + | "CollectionUpdated" + | "CustomerCreated"; + }; + +export type MenuEventSubscriptionFragment_XWulXk1GqeHNvK2Zjg39D81UqhO8ZykBvc7wuJEvA = + { + __typename: + | "CustomerMetadataUpdated" + | "CustomerUpdated" + | "DraftOrderCreated" + | "DraftOrderDeleted" + | "DraftOrderUpdated" + | "FulfillmentApproved" + | "FulfillmentCanceled" + | "FulfillmentCreated" + | "FulfillmentMetadataUpdated" + | "FulfillmentTrackingNumberUpdated" + | "GiftCardCreated" + | "GiftCardDeleted" + | "GiftCardExportCompleted" + | "GiftCardMetadataUpdated" + | "GiftCardSent" + | "GiftCardStatusChanged" + | "GiftCardUpdated" + | "InvoiceDeleted" + | "InvoiceRequested" + | "InvoiceSent"; + }; + +export type MenuEventSubscriptionFragment_6SZv9znezpLhGpS69dQ1GbY5yDKa5XUxykJztqqTg6U = + { + __typename: + | "ListStoredPaymentMethods" + | "OrderBulkCreated" + | "OrderCancelled" + | "OrderConfirmed" + | "OrderCreated" + | "OrderExpired" + | "OrderFilterShippingMethods" + | "OrderFulfilled" + | "OrderFullyPaid" + | "OrderFullyRefunded" + | "OrderMetadataUpdated" + | "OrderPaid" + | "OrderRefunded" + | "OrderUpdated" + | "PageCreated" + | "PageDeleted" + | "PageTypeCreated" + | "PageTypeDeleted" + | "PageTypeUpdated" + | "PageUpdated"; + }; + +export type MenuEventSubscriptionFragment_6kRlk3To6sPpW0QQr52mUNjVn2CzSdpyN0o8Cy5kQ70 = + { + __typename: + | "PaymentAuthorize" + | "PaymentCaptureEvent" + | "PaymentConfirmEvent" + | "PaymentGatewayInitializeSession" + | "PaymentGatewayInitializeTokenizationSession" + | "PaymentListGateways" + | "PaymentMethodInitializeTokenizationSession" + | "PaymentMethodProcessTokenizationSession" + | "PaymentProcessEvent" + | "PaymentRefundEvent" + | "PaymentVoidEvent" + | "PermissionGroupCreated" + | "PermissionGroupDeleted" + | "PermissionGroupUpdated" + | "ProductCreated" + | "ProductDeleted" + | "ProductExportCompleted" + | "ProductMediaCreated" + | "ProductMediaDeleted" + | "ProductMediaUpdated"; + }; + +export type MenuEventSubscriptionFragment_Vj0rLuHjNAcQ6LqPpibXk9LyMpl7ObVs0GDdoBpJh4 = + { + __typename: + | "ProductMetadataUpdated" + | "ProductUpdated" + | "ProductVariantBackInStock" + | "ProductVariantCreated" + | "ProductVariantDeleted" + | "ProductVariantDiscountedPriceUpdated" + | "ProductVariantMetadataUpdated" + | "ProductVariantOutOfStock" + | "ProductVariantStockUpdated" + | "ProductVariantUpdated" + | "PromotionCreated" + | "PromotionDeleted" + | "PromotionEnded" + | "PromotionRuleCreated" + | "PromotionRuleDeleted" + | "PromotionRuleUpdated" + | "PromotionStarted" + | "PromotionUpdated" + | "SaleCreated" + | "SaleDeleted"; + }; + +export type MenuEventSubscriptionFragment_6CiRoIuh4Yp4Dw8YuZkIoAi7nacE1LspcEvEinZSlh0 = + { + __typename: + | "SaleToggle" + | "SaleUpdated" + | "ShippingListMethodsForCheckout" + | "ShippingPriceCreated" + | "ShippingPriceDeleted" + | "ShippingPriceUpdated" + | "ShippingZoneCreated" + | "ShippingZoneDeleted" + | "ShippingZoneMetadataUpdated" + | "ShippingZoneUpdated" + | "ShopMetadataUpdated" + | "StaffCreated" + | "StaffDeleted" + | "StaffSetPasswordRequested" + | "StaffUpdated" + | "StoredPaymentMethodDeleteRequested" + | "ThumbnailCreated" + | "TransactionCancelationRequested" + | "TransactionChargeRequested" + | "TransactionInitializeSession"; + }; + +export type MenuEventSubscriptionFragment_FzRpvXbZsLUnyQcwg2iTzgAMy1kmkcQ76jDvP0ZVoA = + { + __typename: + | "TransactionItemMetadataUpdated" + | "TransactionProcessSession" + | "TransactionRefundRequested" + | "TranslationCreated" + | "TranslationUpdated" + | "VoucherCodeExportCompleted" + | "VoucherCodesCreated" + | "VoucherCodesDeleted" + | "VoucherCreated" + | "VoucherDeleted" + | "VoucherMetadataUpdated" + | "VoucherUpdated" + | "WarehouseCreated" + | "WarehouseDeleted" + | "WarehouseMetadataUpdated" + | "WarehouseUpdated"; + }; + +export type MenuEventSubscriptionFragment_MenuCreated = { + menu: MenuEventSubscriptionFragment_MenuCreated_menu_Menu | null; +} & { __typename: "MenuCreated" }; + +export type MenuEventSubscriptionFragment_MenuDeleted = { + menu: MenuEventSubscriptionFragment_MenuDeleted_menu_Menu | null; +} & { __typename: "MenuDeleted" }; + +export type MenuEventSubscriptionFragment_MenuItemCreated = { + menuItem: MenuEventSubscriptionFragment_MenuItemCreated_menuItem_MenuItem | null; +} & { __typename: "MenuItemCreated" }; + +export type MenuEventSubscriptionFragment_MenuItemDeleted = { + menuItem: MenuEventSubscriptionFragment_MenuItemDeleted_menuItem_MenuItem | null; +} & { __typename: "MenuItemDeleted" }; + +export type MenuEventSubscriptionFragment_MenuItemUpdated = { + menuItem: MenuEventSubscriptionFragment_MenuItemUpdated_menuItem_MenuItem | null; +} & { __typename: "MenuItemUpdated" }; + +export type MenuEventSubscriptionFragment_MenuUpdated = { + menu: MenuEventSubscriptionFragment_MenuUpdated_menu_Menu | null; +} & { __typename: "MenuUpdated" }; export type MenuEventSubscriptionFragment = | MenuEventSubscriptionFragment_Uchm3Qz7YjEsQhTMfPIk01DEzLiWluHMnX4k1L6Dt0s @@ -114,76 +486,261 @@ export type MenuEventSubscriptionFragment = | MenuEventSubscriptionFragment_XWulXk1GqeHNvK2Zjg39D81UqhO8ZykBvc7wuJEvA | MenuEventSubscriptionFragment_6SZv9znezpLhGpS69dQ1GbY5yDKa5XUxykJztqqTg6U | MenuEventSubscriptionFragment_6kRlk3To6sPpW0QQr52mUNjVn2CzSdpyN0o8Cy5kQ70 - | MenuEventSubscriptionFragment_Rhl3UnNk6nEi747MfPtEghacMvrlEq0zkU4BVjs - | MenuEventSubscriptionFragment_W5T9u8Ze80BUkN79Oq6SmIzz96pnf5x7CafMXoKzjye - | MenuEventSubscriptionFragment_YvQxrNkoOvnbjiQk7JSzAwJglAbMml79KptInbPmJ50 + | MenuEventSubscriptionFragment_Vj0rLuHjNAcQ6LqPpibXk9LyMpl7ObVs0GDdoBpJh4 + | MenuEventSubscriptionFragment_6CiRoIuh4Yp4Dw8YuZkIoAi7nacE1LspcEvEinZSlh0 + | MenuEventSubscriptionFragment_FzRpvXbZsLUnyQcwg2iTzgAMy1kmkcQ76jDvP0ZVoA | MenuEventSubscriptionFragment_MenuCreated | MenuEventSubscriptionFragment_MenuDeleted | MenuEventSubscriptionFragment_MenuItemCreated | MenuEventSubscriptionFragment_MenuItemDeleted | MenuEventSubscriptionFragment_MenuItemUpdated - | MenuEventSubscriptionFragment_MenuUpdated -; - -export type Money = { currency: string, amount: number }; - -export type PageEventSubscriptionFragment_PageCreated_page_Page = { slug: string }; - -export type PageEventSubscriptionFragment_PageDeleted_page_Page = { slug: string }; - -export type PageEventSubscriptionFragment_PageTypeCreated_pageType_PageType = { slug: string }; - -export type PageEventSubscriptionFragment_PageTypeDeleted_pageType_PageType = { slug: string }; - -export type PageEventSubscriptionFragment_PageTypeUpdated_pageType_PageType = { slug: string }; - -export type PageEventSubscriptionFragment_PageUpdated_page_Page = { slug: string }; - -export type PageEventSubscriptionFragment_Uchm3Qz7YjEsQhTMfPIk01DEzLiWluHMnX4k1L6Dt0s = { __typename: 'AccountChangeEmailRequested' | 'AccountConfirmationRequested' | 'AccountConfirmed' | 'AccountDeleteRequested' | 'AccountDeleted' | 'AccountEmailChanged' | 'AccountSetPasswordRequested' | 'AddressCreated' | 'AddressDeleted' | 'AddressUpdated' | 'AppDeleted' | 'AppInstalled' | 'AppStatusChanged' | 'AppUpdated' | 'AttributeCreated' | 'AttributeDeleted' | 'AttributeUpdated' | 'AttributeValueCreated' | 'AttributeValueDeleted' | 'AttributeValueUpdated' }; - -export type PageEventSubscriptionFragment_GwYHqJDwvrv2QyEq0Kya5B6RfhCy85iuAlJzAq6AdU = { __typename: 'CalculateTaxes' | 'CategoryCreated' | 'CategoryDeleted' | 'CategoryUpdated' | 'ChannelCreated' | 'ChannelDeleted' | 'ChannelMetadataUpdated' | 'ChannelStatusChanged' | 'ChannelUpdated' | 'CheckoutCreated' | 'CheckoutFilterShippingMethods' | 'CheckoutFullyAuthorized' | 'CheckoutFullyPaid' | 'CheckoutMetadataUpdated' | 'CheckoutUpdated' | 'CollectionCreated' | 'CollectionDeleted' | 'CollectionMetadataUpdated' | 'CollectionUpdated' | 'CustomerCreated' }; - -export type PageEventSubscriptionFragment_XWulXk1GqeHNvK2Zjg39D81UqhO8ZykBvc7wuJEvA = { __typename: 'CustomerMetadataUpdated' | 'CustomerUpdated' | 'DraftOrderCreated' | 'DraftOrderDeleted' | 'DraftOrderUpdated' | 'FulfillmentApproved' | 'FulfillmentCanceled' | 'FulfillmentCreated' | 'FulfillmentMetadataUpdated' | 'FulfillmentTrackingNumberUpdated' | 'GiftCardCreated' | 'GiftCardDeleted' | 'GiftCardExportCompleted' | 'GiftCardMetadataUpdated' | 'GiftCardSent' | 'GiftCardStatusChanged' | 'GiftCardUpdated' | 'InvoiceDeleted' | 'InvoiceRequested' | 'InvoiceSent' }; - -export type PageEventSubscriptionFragment_Qo3grqPrpe4HInn1EwEhNaiRstQso5tTjYam1lLlKa = { __typename: 'ListStoredPaymentMethods' | 'MenuCreated' | 'MenuDeleted' | 'MenuItemCreated' | 'MenuItemDeleted' | 'MenuItemUpdated' | 'MenuUpdated' | 'OrderBulkCreated' | 'OrderCancelled' | 'OrderConfirmed' | 'OrderCreated' | 'OrderExpired' | 'OrderFilterShippingMethods' | 'OrderFulfilled' | 'OrderFullyPaid' | 'OrderFullyRefunded' | 'OrderMetadataUpdated' | 'OrderPaid' | 'OrderRefunded' | 'OrderUpdated' }; - -export type PageEventSubscriptionFragment_6kRlk3To6sPpW0QQr52mUNjVn2CzSdpyN0o8Cy5kQ70 = { __typename: 'PaymentAuthorize' | 'PaymentCaptureEvent' | 'PaymentConfirmEvent' | 'PaymentGatewayInitializeSession' | 'PaymentGatewayInitializeTokenizationSession' | 'PaymentListGateways' | 'PaymentMethodInitializeTokenizationSession' | 'PaymentMethodProcessTokenizationSession' | 'PaymentProcessEvent' | 'PaymentRefundEvent' | 'PaymentVoidEvent' | 'PermissionGroupCreated' | 'PermissionGroupDeleted' | 'PermissionGroupUpdated' | 'ProductCreated' | 'ProductDeleted' | 'ProductExportCompleted' | 'ProductMediaCreated' | 'ProductMediaDeleted' | 'ProductMediaUpdated' }; - -export type PageEventSubscriptionFragment_Rhl3UnNk6nEi747MfPtEghacMvrlEq0zkU4BVjs = { __typename: 'ProductMetadataUpdated' | 'ProductUpdated' | 'ProductVariantBackInStock' | 'ProductVariantCreated' | 'ProductVariantDeleted' | 'ProductVariantMetadataUpdated' | 'ProductVariantOutOfStock' | 'ProductVariantStockUpdated' | 'ProductVariantUpdated' | 'PromotionCreated' | 'PromotionDeleted' | 'PromotionEnded' | 'PromotionRuleCreated' | 'PromotionRuleDeleted' | 'PromotionRuleUpdated' | 'PromotionStarted' | 'PromotionUpdated' | 'SaleCreated' | 'SaleDeleted' | 'SaleToggle' }; - -export type PageEventSubscriptionFragment_W5T9u8Ze80BUkN79Oq6SmIzz96pnf5x7CafMXoKzjye = { __typename: 'SaleUpdated' | 'ShippingListMethodsForCheckout' | 'ShippingPriceCreated' | 'ShippingPriceDeleted' | 'ShippingPriceUpdated' | 'ShippingZoneCreated' | 'ShippingZoneDeleted' | 'ShippingZoneMetadataUpdated' | 'ShippingZoneUpdated' | 'ShopMetadataUpdated' | 'StaffCreated' | 'StaffDeleted' | 'StaffSetPasswordRequested' | 'StaffUpdated' | 'StoredPaymentMethodDeleteRequested' | 'ThumbnailCreated' | 'TransactionCancelationRequested' | 'TransactionChargeRequested' | 'TransactionInitializeSession' | 'TransactionItemMetadataUpdated' }; - -export type PageEventSubscriptionFragment_YvQxrNkoOvnbjiQk7JSzAwJglAbMml79KptInbPmJ50 = { __typename: 'TransactionProcessSession' | 'TransactionRefundRequested' | 'TranslationCreated' | 'TranslationUpdated' | 'VoucherCodeExportCompleted' | 'VoucherCodesCreated' | 'VoucherCodesDeleted' | 'VoucherCreated' | 'VoucherDeleted' | 'VoucherMetadataUpdated' | 'VoucherUpdated' | 'WarehouseCreated' | 'WarehouseDeleted' | 'WarehouseMetadataUpdated' | 'WarehouseUpdated' }; - -export type PageEventSubscriptionFragment_PageCreated = ( - { page: PageEventSubscriptionFragment_PageCreated_page_Page | null } - & { __typename: 'PageCreated' } -); - -export type PageEventSubscriptionFragment_PageDeleted = ( - { page: PageEventSubscriptionFragment_PageDeleted_page_Page | null } - & { __typename: 'PageDeleted' } -); - -export type PageEventSubscriptionFragment_PageTypeCreated = ( - { pageType: PageEventSubscriptionFragment_PageTypeCreated_pageType_PageType | null } - & { __typename: 'PageTypeCreated' } -); - -export type PageEventSubscriptionFragment_PageTypeDeleted = ( - { pageType: PageEventSubscriptionFragment_PageTypeDeleted_pageType_PageType | null } - & { __typename: 'PageTypeDeleted' } -); - -export type PageEventSubscriptionFragment_PageTypeUpdated = ( - { pageType: PageEventSubscriptionFragment_PageTypeUpdated_pageType_PageType | null } - & { __typename: 'PageTypeUpdated' } -); - -export type PageEventSubscriptionFragment_PageUpdated = ( - { page: PageEventSubscriptionFragment_PageUpdated_page_Page | null } - & { __typename: 'PageUpdated' } -); + | MenuEventSubscriptionFragment_MenuUpdated; + +export type Money = { currency: string; amount: number }; + +export type PageEventSubscriptionFragment_PageCreated_page_Page = { + slug: string; +}; + +export type PageEventSubscriptionFragment_PageDeleted_page_Page = { + slug: string; +}; + +export type PageEventSubscriptionFragment_PageTypeCreated_pageType_PageType = { + slug: string; +}; + +export type PageEventSubscriptionFragment_PageTypeDeleted_pageType_PageType = { + slug: string; +}; + +export type PageEventSubscriptionFragment_PageTypeUpdated_pageType_PageType = { + slug: string; +}; + +export type PageEventSubscriptionFragment_PageUpdated_page_Page = { + slug: string; +}; + +export type PageEventSubscriptionFragment_Uchm3Qz7YjEsQhTMfPIk01DEzLiWluHMnX4k1L6Dt0s = + { + __typename: + | "AccountChangeEmailRequested" + | "AccountConfirmationRequested" + | "AccountConfirmed" + | "AccountDeleteRequested" + | "AccountDeleted" + | "AccountEmailChanged" + | "AccountSetPasswordRequested" + | "AddressCreated" + | "AddressDeleted" + | "AddressUpdated" + | "AppDeleted" + | "AppInstalled" + | "AppStatusChanged" + | "AppUpdated" + | "AttributeCreated" + | "AttributeDeleted" + | "AttributeUpdated" + | "AttributeValueCreated" + | "AttributeValueDeleted" + | "AttributeValueUpdated"; + }; + +export type PageEventSubscriptionFragment_GwYHqJDwvrv2QyEq0Kya5B6RfhCy85iuAlJzAq6AdU = + { + __typename: + | "CalculateTaxes" + | "CategoryCreated" + | "CategoryDeleted" + | "CategoryUpdated" + | "ChannelCreated" + | "ChannelDeleted" + | "ChannelMetadataUpdated" + | "ChannelStatusChanged" + | "ChannelUpdated" + | "CheckoutCreated" + | "CheckoutFilterShippingMethods" + | "CheckoutFullyAuthorized" + | "CheckoutFullyPaid" + | "CheckoutMetadataUpdated" + | "CheckoutUpdated" + | "CollectionCreated" + | "CollectionDeleted" + | "CollectionMetadataUpdated" + | "CollectionUpdated" + | "CustomerCreated"; + }; + +export type PageEventSubscriptionFragment_XWulXk1GqeHNvK2Zjg39D81UqhO8ZykBvc7wuJEvA = + { + __typename: + | "CustomerMetadataUpdated" + | "CustomerUpdated" + | "DraftOrderCreated" + | "DraftOrderDeleted" + | "DraftOrderUpdated" + | "FulfillmentApproved" + | "FulfillmentCanceled" + | "FulfillmentCreated" + | "FulfillmentMetadataUpdated" + | "FulfillmentTrackingNumberUpdated" + | "GiftCardCreated" + | "GiftCardDeleted" + | "GiftCardExportCompleted" + | "GiftCardMetadataUpdated" + | "GiftCardSent" + | "GiftCardStatusChanged" + | "GiftCardUpdated" + | "InvoiceDeleted" + | "InvoiceRequested" + | "InvoiceSent"; + }; + +export type PageEventSubscriptionFragment_Qo3grqPrpe4HInn1EwEhNaiRstQso5tTjYam1lLlKa = + { + __typename: + | "ListStoredPaymentMethods" + | "MenuCreated" + | "MenuDeleted" + | "MenuItemCreated" + | "MenuItemDeleted" + | "MenuItemUpdated" + | "MenuUpdated" + | "OrderBulkCreated" + | "OrderCancelled" + | "OrderConfirmed" + | "OrderCreated" + | "OrderExpired" + | "OrderFilterShippingMethods" + | "OrderFulfilled" + | "OrderFullyPaid" + | "OrderFullyRefunded" + | "OrderMetadataUpdated" + | "OrderPaid" + | "OrderRefunded" + | "OrderUpdated"; + }; + +export type PageEventSubscriptionFragment_6kRlk3To6sPpW0QQr52mUNjVn2CzSdpyN0o8Cy5kQ70 = + { + __typename: + | "PaymentAuthorize" + | "PaymentCaptureEvent" + | "PaymentConfirmEvent" + | "PaymentGatewayInitializeSession" + | "PaymentGatewayInitializeTokenizationSession" + | "PaymentListGateways" + | "PaymentMethodInitializeTokenizationSession" + | "PaymentMethodProcessTokenizationSession" + | "PaymentProcessEvent" + | "PaymentRefundEvent" + | "PaymentVoidEvent" + | "PermissionGroupCreated" + | "PermissionGroupDeleted" + | "PermissionGroupUpdated" + | "ProductCreated" + | "ProductDeleted" + | "ProductExportCompleted" + | "ProductMediaCreated" + | "ProductMediaDeleted" + | "ProductMediaUpdated"; + }; + +export type PageEventSubscriptionFragment_Vj0rLuHjNAcQ6LqPpibXk9LyMpl7ObVs0GDdoBpJh4 = + { + __typename: + | "ProductMetadataUpdated" + | "ProductUpdated" + | "ProductVariantBackInStock" + | "ProductVariantCreated" + | "ProductVariantDeleted" + | "ProductVariantDiscountedPriceUpdated" + | "ProductVariantMetadataUpdated" + | "ProductVariantOutOfStock" + | "ProductVariantStockUpdated" + | "ProductVariantUpdated" + | "PromotionCreated" + | "PromotionDeleted" + | "PromotionEnded" + | "PromotionRuleCreated" + | "PromotionRuleDeleted" + | "PromotionRuleUpdated" + | "PromotionStarted" + | "PromotionUpdated" + | "SaleCreated" + | "SaleDeleted"; + }; + +export type PageEventSubscriptionFragment_6CiRoIuh4Yp4Dw8YuZkIoAi7nacE1LspcEvEinZSlh0 = + { + __typename: + | "SaleToggle" + | "SaleUpdated" + | "ShippingListMethodsForCheckout" + | "ShippingPriceCreated" + | "ShippingPriceDeleted" + | "ShippingPriceUpdated" + | "ShippingZoneCreated" + | "ShippingZoneDeleted" + | "ShippingZoneMetadataUpdated" + | "ShippingZoneUpdated" + | "ShopMetadataUpdated" + | "StaffCreated" + | "StaffDeleted" + | "StaffSetPasswordRequested" + | "StaffUpdated" + | "StoredPaymentMethodDeleteRequested" + | "ThumbnailCreated" + | "TransactionCancelationRequested" + | "TransactionChargeRequested" + | "TransactionInitializeSession"; + }; + +export type PageEventSubscriptionFragment_FzRpvXbZsLUnyQcwg2iTzgAMy1kmkcQ76jDvP0ZVoA = + { + __typename: + | "TransactionItemMetadataUpdated" + | "TransactionProcessSession" + | "TransactionRefundRequested" + | "TranslationCreated" + | "TranslationUpdated" + | "VoucherCodeExportCompleted" + | "VoucherCodesCreated" + | "VoucherCodesDeleted" + | "VoucherCreated" + | "VoucherDeleted" + | "VoucherMetadataUpdated" + | "VoucherUpdated" + | "WarehouseCreated" + | "WarehouseDeleted" + | "WarehouseMetadataUpdated" + | "WarehouseUpdated"; + }; + +export type PageEventSubscriptionFragment_PageCreated = { + page: PageEventSubscriptionFragment_PageCreated_page_Page | null; +} & { __typename: "PageCreated" }; + +export type PageEventSubscriptionFragment_PageDeleted = { + page: PageEventSubscriptionFragment_PageDeleted_page_Page | null; +} & { __typename: "PageDeleted" }; + +export type PageEventSubscriptionFragment_PageTypeCreated = { + pageType: PageEventSubscriptionFragment_PageTypeCreated_pageType_PageType | null; +} & { __typename: "PageTypeCreated" }; + +export type PageEventSubscriptionFragment_PageTypeDeleted = { + pageType: PageEventSubscriptionFragment_PageTypeDeleted_pageType_PageType | null; +} & { __typename: "PageTypeDeleted" }; + +export type PageEventSubscriptionFragment_PageTypeUpdated = { + pageType: PageEventSubscriptionFragment_PageTypeUpdated_pageType_PageType | null; +} & { __typename: "PageTypeUpdated" }; + +export type PageEventSubscriptionFragment_PageUpdated = { + page: PageEventSubscriptionFragment_PageUpdated_page_Page | null; +} & { __typename: "PageUpdated" }; export type PageEventSubscriptionFragment = | PageEventSubscriptionFragment_Uchm3Qz7YjEsQhTMfPIk01DEzLiWluHMnX4k1L6Dt0s @@ -191,137 +748,332 @@ export type PageEventSubscriptionFragment = | PageEventSubscriptionFragment_XWulXk1GqeHNvK2Zjg39D81UqhO8ZykBvc7wuJEvA | PageEventSubscriptionFragment_Qo3grqPrpe4HInn1EwEhNaiRstQso5tTjYam1lLlKa | PageEventSubscriptionFragment_6kRlk3To6sPpW0QQr52mUNjVn2CzSdpyN0o8Cy5kQ70 - | PageEventSubscriptionFragment_Rhl3UnNk6nEi747MfPtEghacMvrlEq0zkU4BVjs - | PageEventSubscriptionFragment_W5T9u8Ze80BUkN79Oq6SmIzz96pnf5x7CafMXoKzjye - | PageEventSubscriptionFragment_YvQxrNkoOvnbjiQk7JSzAwJglAbMml79KptInbPmJ50 + | PageEventSubscriptionFragment_Vj0rLuHjNAcQ6LqPpibXk9LyMpl7ObVs0GDdoBpJh4 + | PageEventSubscriptionFragment_6CiRoIuh4Yp4Dw8YuZkIoAi7nacE1LspcEvEinZSlh0 + | PageEventSubscriptionFragment_FzRpvXbZsLUnyQcwg2iTzgAMy1kmkcQ76jDvP0ZVoA | PageEventSubscriptionFragment_PageCreated | PageEventSubscriptionFragment_PageDeleted | PageEventSubscriptionFragment_PageTypeCreated | PageEventSubscriptionFragment_PageTypeDeleted | PageEventSubscriptionFragment_PageTypeUpdated - | PageEventSubscriptionFragment_PageUpdated -; - -export type ProductEventSubscriptionFragment_ProductDeleted_product_Product = { slug: string }; - -export type ProductEventSubscriptionFragment_ProductMediaCreated_productMedia_ProductMedia = { productId: string | null }; - -export type ProductEventSubscriptionFragment_ProductMediaDeleted_productMedia_ProductMedia = { productId: string | null }; - -export type ProductEventSubscriptionFragment_ProductMediaUpdated_productMedia_ProductMedia = { productId: string | null }; - -export type ProductEventSubscriptionFragment_ProductMetadataUpdated_product_Product = { slug: string }; - -export type ProductEventSubscriptionFragment_ProductUpdated_product_Product = { slug: string }; - -export type ProductEventSubscriptionFragment_ProductVariantBackInStock_productVariant_ProductVariant_product_Product = { slug: string }; - -export type ProductEventSubscriptionFragment_ProductVariantBackInStock_productVariant_ProductVariant = { product: ProductEventSubscriptionFragment_ProductVariantBackInStock_productVariant_ProductVariant_product_Product }; - -export type ProductEventSubscriptionFragment_ProductVariantCreated_productVariant_ProductVariant_product_Product = { slug: string }; - -export type ProductEventSubscriptionFragment_ProductVariantCreated_productVariant_ProductVariant = { product: ProductEventSubscriptionFragment_ProductVariantCreated_productVariant_ProductVariant_product_Product }; - -export type ProductEventSubscriptionFragment_ProductVariantDeleted_productVariant_ProductVariant_product_Product = { slug: string }; - -export type ProductEventSubscriptionFragment_ProductVariantDeleted_productVariant_ProductVariant = { product: ProductEventSubscriptionFragment_ProductVariantDeleted_productVariant_ProductVariant_product_Product }; - -export type ProductEventSubscriptionFragment_ProductVariantMetadataUpdated_productVariant_ProductVariant_product_Product = { slug: string }; - -export type ProductEventSubscriptionFragment_ProductVariantMetadataUpdated_productVariant_ProductVariant = { product: ProductEventSubscriptionFragment_ProductVariantMetadataUpdated_productVariant_ProductVariant_product_Product }; - -export type ProductEventSubscriptionFragment_ProductVariantOutOfStock_productVariant_ProductVariant_product_Product = { slug: string }; - -export type ProductEventSubscriptionFragment_ProductVariantOutOfStock_productVariant_ProductVariant = { product: ProductEventSubscriptionFragment_ProductVariantOutOfStock_productVariant_ProductVariant_product_Product }; - -export type ProductEventSubscriptionFragment_ProductVariantStockUpdated_productVariant_ProductVariant_product_Product = { slug: string }; - -export type ProductEventSubscriptionFragment_ProductVariantStockUpdated_productVariant_ProductVariant = { product: ProductEventSubscriptionFragment_ProductVariantStockUpdated_productVariant_ProductVariant_product_Product }; - -export type ProductEventSubscriptionFragment_ProductVariantUpdated_productVariant_ProductVariant_product_Product = { slug: string }; - -export type ProductEventSubscriptionFragment_ProductVariantUpdated_productVariant_ProductVariant = { product: ProductEventSubscriptionFragment_ProductVariantUpdated_productVariant_ProductVariant_product_Product }; - -export type ProductEventSubscriptionFragment_Uchm3Qz7YjEsQhTMfPIk01DEzLiWluHMnX4k1L6Dt0s = { __typename: 'AccountChangeEmailRequested' | 'AccountConfirmationRequested' | 'AccountConfirmed' | 'AccountDeleteRequested' | 'AccountDeleted' | 'AccountEmailChanged' | 'AccountSetPasswordRequested' | 'AddressCreated' | 'AddressDeleted' | 'AddressUpdated' | 'AppDeleted' | 'AppInstalled' | 'AppStatusChanged' | 'AppUpdated' | 'AttributeCreated' | 'AttributeDeleted' | 'AttributeUpdated' | 'AttributeValueCreated' | 'AttributeValueDeleted' | 'AttributeValueUpdated' }; - -export type ProductEventSubscriptionFragment_GwYHqJDwvrv2QyEq0Kya5B6RfhCy85iuAlJzAq6AdU = { __typename: 'CalculateTaxes' | 'CategoryCreated' | 'CategoryDeleted' | 'CategoryUpdated' | 'ChannelCreated' | 'ChannelDeleted' | 'ChannelMetadataUpdated' | 'ChannelStatusChanged' | 'ChannelUpdated' | 'CheckoutCreated' | 'CheckoutFilterShippingMethods' | 'CheckoutFullyAuthorized' | 'CheckoutFullyPaid' | 'CheckoutMetadataUpdated' | 'CheckoutUpdated' | 'CollectionCreated' | 'CollectionDeleted' | 'CollectionMetadataUpdated' | 'CollectionUpdated' | 'CustomerCreated' }; - -export type ProductEventSubscriptionFragment_XWulXk1GqeHNvK2Zjg39D81UqhO8ZykBvc7wuJEvA = { __typename: 'CustomerMetadataUpdated' | 'CustomerUpdated' | 'DraftOrderCreated' | 'DraftOrderDeleted' | 'DraftOrderUpdated' | 'FulfillmentApproved' | 'FulfillmentCanceled' | 'FulfillmentCreated' | 'FulfillmentMetadataUpdated' | 'FulfillmentTrackingNumberUpdated' | 'GiftCardCreated' | 'GiftCardDeleted' | 'GiftCardExportCompleted' | 'GiftCardMetadataUpdated' | 'GiftCardSent' | 'GiftCardStatusChanged' | 'GiftCardUpdated' | 'InvoiceDeleted' | 'InvoiceRequested' | 'InvoiceSent' }; - -export type ProductEventSubscriptionFragment_Qo3grqPrpe4HInn1EwEhNaiRstQso5tTjYam1lLlKa = { __typename: 'ListStoredPaymentMethods' | 'MenuCreated' | 'MenuDeleted' | 'MenuItemCreated' | 'MenuItemDeleted' | 'MenuItemUpdated' | 'MenuUpdated' | 'OrderBulkCreated' | 'OrderCancelled' | 'OrderConfirmed' | 'OrderCreated' | 'OrderExpired' | 'OrderFilterShippingMethods' | 'OrderFulfilled' | 'OrderFullyPaid' | 'OrderFullyRefunded' | 'OrderMetadataUpdated' | 'OrderPaid' | 'OrderRefunded' | 'OrderUpdated' }; - -export type ProductEventSubscriptionFragment_0Hg7UwAf5qDqrfrBkEq72AxaObrB0w2l4xizB32wmho = { __typename: 'PageCreated' | 'PageDeleted' | 'PageTypeCreated' | 'PageTypeDeleted' | 'PageTypeUpdated' | 'PageUpdated' | 'PaymentAuthorize' | 'PaymentCaptureEvent' | 'PaymentConfirmEvent' | 'PaymentGatewayInitializeSession' | 'PaymentGatewayInitializeTokenizationSession' | 'PaymentListGateways' | 'PaymentMethodInitializeTokenizationSession' | 'PaymentMethodProcessTokenizationSession' | 'PaymentProcessEvent' | 'PaymentRefundEvent' | 'PaymentVoidEvent' | 'PermissionGroupCreated' | 'PermissionGroupDeleted' | 'PermissionGroupUpdated' }; - -export type ProductEventSubscriptionFragment_GaRpV5WgLmyYwzvOncnidjvA9BqsuLaqkK8MskS5QAk = { __typename: 'ProductCreated' | 'ProductExportCompleted' | 'PromotionCreated' | 'PromotionDeleted' | 'PromotionEnded' | 'PromotionRuleCreated' | 'PromotionRuleDeleted' | 'PromotionRuleUpdated' | 'PromotionStarted' | 'PromotionUpdated' | 'SaleCreated' | 'SaleDeleted' | 'SaleToggle' | 'SaleUpdated' | 'ShippingListMethodsForCheckout' | 'ShippingPriceCreated' | 'ShippingPriceDeleted' | 'ShippingPriceUpdated' | 'ShippingZoneCreated' | 'ShippingZoneDeleted' }; - -export type ProductEventSubscriptionFragment_HwOxxNnVYeYl4jbk7puxZvo4KTvQJnkcex833Wfg = { __typename: 'ShippingZoneMetadataUpdated' | 'ShippingZoneUpdated' | 'ShopMetadataUpdated' | 'StaffCreated' | 'StaffDeleted' | 'StaffSetPasswordRequested' | 'StaffUpdated' | 'StoredPaymentMethodDeleteRequested' | 'ThumbnailCreated' | 'TransactionCancelationRequested' | 'TransactionChargeRequested' | 'TransactionInitializeSession' | 'TransactionItemMetadataUpdated' | 'TransactionProcessSession' | 'TransactionRefundRequested' | 'TranslationCreated' | 'TranslationUpdated' | 'VoucherCodeExportCompleted' | 'VoucherCodesCreated' | 'VoucherCodesDeleted' }; - -export type ProductEventSubscriptionFragment_90suajkXt8hiTAxfKMvbNnr0Zjq2loXudhjMa1vc = { __typename: 'VoucherCreated' | 'VoucherDeleted' | 'VoucherMetadataUpdated' | 'VoucherUpdated' | 'WarehouseCreated' | 'WarehouseDeleted' | 'WarehouseMetadataUpdated' | 'WarehouseUpdated' }; - -export type ProductEventSubscriptionFragment_ProductDeleted = ( - { product: ProductEventSubscriptionFragment_ProductDeleted_product_Product | null } - & { __typename: 'ProductDeleted' } -); - -export type ProductEventSubscriptionFragment_ProductMediaCreated = ( - { productMedia: ProductEventSubscriptionFragment_ProductMediaCreated_productMedia_ProductMedia | null } - & { __typename: 'ProductMediaCreated' } -); - -export type ProductEventSubscriptionFragment_ProductMediaDeleted = ( - { productMedia: ProductEventSubscriptionFragment_ProductMediaDeleted_productMedia_ProductMedia | null } - & { __typename: 'ProductMediaDeleted' } -); - -export type ProductEventSubscriptionFragment_ProductMediaUpdated = ( - { productMedia: ProductEventSubscriptionFragment_ProductMediaUpdated_productMedia_ProductMedia | null } - & { __typename: 'ProductMediaUpdated' } -); - -export type ProductEventSubscriptionFragment_ProductMetadataUpdated = ( - { product: ProductEventSubscriptionFragment_ProductMetadataUpdated_product_Product | null } - & { __typename: 'ProductMetadataUpdated' } -); - -export type ProductEventSubscriptionFragment_ProductUpdated = ( - { product: ProductEventSubscriptionFragment_ProductUpdated_product_Product | null } - & { __typename: 'ProductUpdated' } -); - -export type ProductEventSubscriptionFragment_ProductVariantBackInStock = ( - { productVariant: ProductEventSubscriptionFragment_ProductVariantBackInStock_productVariant_ProductVariant | null } - & { __typename: 'ProductVariantBackInStock' } -); - -export type ProductEventSubscriptionFragment_ProductVariantCreated = ( - { productVariant: ProductEventSubscriptionFragment_ProductVariantCreated_productVariant_ProductVariant | null } - & { __typename: 'ProductVariantCreated' } -); - -export type ProductEventSubscriptionFragment_ProductVariantDeleted = ( - { productVariant: ProductEventSubscriptionFragment_ProductVariantDeleted_productVariant_ProductVariant | null } - & { __typename: 'ProductVariantDeleted' } -); - -export type ProductEventSubscriptionFragment_ProductVariantMetadataUpdated = ( - { productVariant: ProductEventSubscriptionFragment_ProductVariantMetadataUpdated_productVariant_ProductVariant | null } - & { __typename: 'ProductVariantMetadataUpdated' } -); - -export type ProductEventSubscriptionFragment_ProductVariantOutOfStock = ( - { productVariant: ProductEventSubscriptionFragment_ProductVariantOutOfStock_productVariant_ProductVariant | null } - & { __typename: 'ProductVariantOutOfStock' } -); - -export type ProductEventSubscriptionFragment_ProductVariantStockUpdated = ( - { productVariant: ProductEventSubscriptionFragment_ProductVariantStockUpdated_productVariant_ProductVariant | null } - & { __typename: 'ProductVariantStockUpdated' } -); - -export type ProductEventSubscriptionFragment_ProductVariantUpdated = ( - { productVariant: ProductEventSubscriptionFragment_ProductVariantUpdated_productVariant_ProductVariant | null } - & { __typename: 'ProductVariantUpdated' } -); + | PageEventSubscriptionFragment_PageUpdated; + +export type ProductEventSubscriptionFragment_ProductDeleted_product_Product = { + slug: string; +}; + +export type ProductEventSubscriptionFragment_ProductMediaCreated_productMedia_ProductMedia = + { productId: string | null }; + +export type ProductEventSubscriptionFragment_ProductMediaDeleted_productMedia_ProductMedia = + { productId: string | null }; + +export type ProductEventSubscriptionFragment_ProductMediaUpdated_productMedia_ProductMedia = + { productId: string | null }; + +export type ProductEventSubscriptionFragment_ProductMetadataUpdated_product_Product = + { slug: string }; + +export type ProductEventSubscriptionFragment_ProductUpdated_product_Product = { + slug: string; +}; + +export type ProductEventSubscriptionFragment_ProductVariantBackInStock_productVariant_ProductVariant_product_Product = + { slug: string }; + +export type ProductEventSubscriptionFragment_ProductVariantBackInStock_productVariant_ProductVariant = + { + product: ProductEventSubscriptionFragment_ProductVariantBackInStock_productVariant_ProductVariant_product_Product; + }; + +export type ProductEventSubscriptionFragment_ProductVariantCreated_productVariant_ProductVariant_product_Product = + { slug: string }; + +export type ProductEventSubscriptionFragment_ProductVariantCreated_productVariant_ProductVariant = + { + product: ProductEventSubscriptionFragment_ProductVariantCreated_productVariant_ProductVariant_product_Product; + }; + +export type ProductEventSubscriptionFragment_ProductVariantDeleted_productVariant_ProductVariant_product_Product = + { slug: string }; + +export type ProductEventSubscriptionFragment_ProductVariantDeleted_productVariant_ProductVariant = + { + product: ProductEventSubscriptionFragment_ProductVariantDeleted_productVariant_ProductVariant_product_Product; + }; + +export type ProductEventSubscriptionFragment_ProductVariantMetadataUpdated_productVariant_ProductVariant_product_Product = + { slug: string }; + +export type ProductEventSubscriptionFragment_ProductVariantMetadataUpdated_productVariant_ProductVariant = + { + product: ProductEventSubscriptionFragment_ProductVariantMetadataUpdated_productVariant_ProductVariant_product_Product; + }; + +export type ProductEventSubscriptionFragment_ProductVariantOutOfStock_productVariant_ProductVariant_product_Product = + { slug: string }; + +export type ProductEventSubscriptionFragment_ProductVariantOutOfStock_productVariant_ProductVariant = + { + product: ProductEventSubscriptionFragment_ProductVariantOutOfStock_productVariant_ProductVariant_product_Product; + }; + +export type ProductEventSubscriptionFragment_ProductVariantStockUpdated_productVariant_ProductVariant_product_Product = + { slug: string }; + +export type ProductEventSubscriptionFragment_ProductVariantStockUpdated_productVariant_ProductVariant = + { + product: ProductEventSubscriptionFragment_ProductVariantStockUpdated_productVariant_ProductVariant_product_Product; + }; + +export type ProductEventSubscriptionFragment_ProductVariantUpdated_productVariant_ProductVariant_product_Product = + { slug: string }; + +export type ProductEventSubscriptionFragment_ProductVariantUpdated_productVariant_ProductVariant = + { + product: ProductEventSubscriptionFragment_ProductVariantUpdated_productVariant_ProductVariant_product_Product; + }; + +export type ProductEventSubscriptionFragment_Uchm3Qz7YjEsQhTMfPIk01DEzLiWluHMnX4k1L6Dt0s = + { + __typename: + | "AccountChangeEmailRequested" + | "AccountConfirmationRequested" + | "AccountConfirmed" + | "AccountDeleteRequested" + | "AccountDeleted" + | "AccountEmailChanged" + | "AccountSetPasswordRequested" + | "AddressCreated" + | "AddressDeleted" + | "AddressUpdated" + | "AppDeleted" + | "AppInstalled" + | "AppStatusChanged" + | "AppUpdated" + | "AttributeCreated" + | "AttributeDeleted" + | "AttributeUpdated" + | "AttributeValueCreated" + | "AttributeValueDeleted" + | "AttributeValueUpdated"; + }; + +export type ProductEventSubscriptionFragment_GwYHqJDwvrv2QyEq0Kya5B6RfhCy85iuAlJzAq6AdU = + { + __typename: + | "CalculateTaxes" + | "CategoryCreated" + | "CategoryDeleted" + | "CategoryUpdated" + | "ChannelCreated" + | "ChannelDeleted" + | "ChannelMetadataUpdated" + | "ChannelStatusChanged" + | "ChannelUpdated" + | "CheckoutCreated" + | "CheckoutFilterShippingMethods" + | "CheckoutFullyAuthorized" + | "CheckoutFullyPaid" + | "CheckoutMetadataUpdated" + | "CheckoutUpdated" + | "CollectionCreated" + | "CollectionDeleted" + | "CollectionMetadataUpdated" + | "CollectionUpdated" + | "CustomerCreated"; + }; + +export type ProductEventSubscriptionFragment_XWulXk1GqeHNvK2Zjg39D81UqhO8ZykBvc7wuJEvA = + { + __typename: + | "CustomerMetadataUpdated" + | "CustomerUpdated" + | "DraftOrderCreated" + | "DraftOrderDeleted" + | "DraftOrderUpdated" + | "FulfillmentApproved" + | "FulfillmentCanceled" + | "FulfillmentCreated" + | "FulfillmentMetadataUpdated" + | "FulfillmentTrackingNumberUpdated" + | "GiftCardCreated" + | "GiftCardDeleted" + | "GiftCardExportCompleted" + | "GiftCardMetadataUpdated" + | "GiftCardSent" + | "GiftCardStatusChanged" + | "GiftCardUpdated" + | "InvoiceDeleted" + | "InvoiceRequested" + | "InvoiceSent"; + }; + +export type ProductEventSubscriptionFragment_Qo3grqPrpe4HInn1EwEhNaiRstQso5tTjYam1lLlKa = + { + __typename: + | "ListStoredPaymentMethods" + | "MenuCreated" + | "MenuDeleted" + | "MenuItemCreated" + | "MenuItemDeleted" + | "MenuItemUpdated" + | "MenuUpdated" + | "OrderBulkCreated" + | "OrderCancelled" + | "OrderConfirmed" + | "OrderCreated" + | "OrderExpired" + | "OrderFilterShippingMethods" + | "OrderFulfilled" + | "OrderFullyPaid" + | "OrderFullyRefunded" + | "OrderMetadataUpdated" + | "OrderPaid" + | "OrderRefunded" + | "OrderUpdated"; + }; + +export type ProductEventSubscriptionFragment_0Hg7UwAf5qDqrfrBkEq72AxaObrB0w2l4xizB32wmho = + { + __typename: + | "PageCreated" + | "PageDeleted" + | "PageTypeCreated" + | "PageTypeDeleted" + | "PageTypeUpdated" + | "PageUpdated" + | "PaymentAuthorize" + | "PaymentCaptureEvent" + | "PaymentConfirmEvent" + | "PaymentGatewayInitializeSession" + | "PaymentGatewayInitializeTokenizationSession" + | "PaymentListGateways" + | "PaymentMethodInitializeTokenizationSession" + | "PaymentMethodProcessTokenizationSession" + | "PaymentProcessEvent" + | "PaymentRefundEvent" + | "PaymentVoidEvent" + | "PermissionGroupCreated" + | "PermissionGroupDeleted" + | "PermissionGroupUpdated"; + }; + +export type ProductEventSubscriptionFragment_8vnEs7OoSiff0XfWWmMcVlo7I5RldGpA4Gzu0Yf0 = + { + __typename: + | "ProductCreated" + | "ProductExportCompleted" + | "ProductVariantDiscountedPriceUpdated" + | "PromotionCreated" + | "PromotionDeleted" + | "PromotionEnded" + | "PromotionRuleCreated" + | "PromotionRuleDeleted" + | "PromotionRuleUpdated" + | "PromotionStarted" + | "PromotionUpdated" + | "SaleCreated" + | "SaleDeleted" + | "SaleToggle" + | "SaleUpdated" + | "ShippingListMethodsForCheckout" + | "ShippingPriceCreated" + | "ShippingPriceDeleted" + | "ShippingPriceUpdated" + | "ShippingZoneCreated"; + }; + +export type ProductEventSubscriptionFragment_UGfT5W4pN8hUpJmdIVnWUjEHvpe0Yu0P6Z3bGk3l2aY = + { + __typename: + | "ShippingZoneDeleted" + | "ShippingZoneMetadataUpdated" + | "ShippingZoneUpdated" + | "ShopMetadataUpdated" + | "StaffCreated" + | "StaffDeleted" + | "StaffSetPasswordRequested" + | "StaffUpdated" + | "StoredPaymentMethodDeleteRequested" + | "ThumbnailCreated" + | "TransactionCancelationRequested" + | "TransactionChargeRequested" + | "TransactionInitializeSession" + | "TransactionItemMetadataUpdated" + | "TransactionProcessSession" + | "TransactionRefundRequested" + | "TranslationCreated" + | "TranslationUpdated" + | "VoucherCodeExportCompleted" + | "VoucherCodesCreated"; + }; + +export type ProductEventSubscriptionFragment_NEkHKuNaPv7LMkfTm0zqQlkVvSmdwmdm3Po4HHoTe = + { + __typename: + | "VoucherCodesDeleted" + | "VoucherCreated" + | "VoucherDeleted" + | "VoucherMetadataUpdated" + | "VoucherUpdated" + | "WarehouseCreated" + | "WarehouseDeleted" + | "WarehouseMetadataUpdated" + | "WarehouseUpdated"; + }; + +export type ProductEventSubscriptionFragment_ProductDeleted = { + product: ProductEventSubscriptionFragment_ProductDeleted_product_Product | null; +} & { __typename: "ProductDeleted" }; + +export type ProductEventSubscriptionFragment_ProductMediaCreated = { + productMedia: ProductEventSubscriptionFragment_ProductMediaCreated_productMedia_ProductMedia | null; +} & { __typename: "ProductMediaCreated" }; + +export type ProductEventSubscriptionFragment_ProductMediaDeleted = { + productMedia: ProductEventSubscriptionFragment_ProductMediaDeleted_productMedia_ProductMedia | null; +} & { __typename: "ProductMediaDeleted" }; + +export type ProductEventSubscriptionFragment_ProductMediaUpdated = { + productMedia: ProductEventSubscriptionFragment_ProductMediaUpdated_productMedia_ProductMedia | null; +} & { __typename: "ProductMediaUpdated" }; + +export type ProductEventSubscriptionFragment_ProductMetadataUpdated = { + product: ProductEventSubscriptionFragment_ProductMetadataUpdated_product_Product | null; +} & { __typename: "ProductMetadataUpdated" }; + +export type ProductEventSubscriptionFragment_ProductUpdated = { + product: ProductEventSubscriptionFragment_ProductUpdated_product_Product | null; +} & { __typename: "ProductUpdated" }; + +export type ProductEventSubscriptionFragment_ProductVariantBackInStock = { + productVariant: ProductEventSubscriptionFragment_ProductVariantBackInStock_productVariant_ProductVariant | null; +} & { __typename: "ProductVariantBackInStock" }; + +export type ProductEventSubscriptionFragment_ProductVariantCreated = { + productVariant: ProductEventSubscriptionFragment_ProductVariantCreated_productVariant_ProductVariant | null; +} & { __typename: "ProductVariantCreated" }; + +export type ProductEventSubscriptionFragment_ProductVariantDeleted = { + productVariant: ProductEventSubscriptionFragment_ProductVariantDeleted_productVariant_ProductVariant | null; +} & { __typename: "ProductVariantDeleted" }; + +export type ProductEventSubscriptionFragment_ProductVariantMetadataUpdated = { + productVariant: ProductEventSubscriptionFragment_ProductVariantMetadataUpdated_productVariant_ProductVariant | null; +} & { __typename: "ProductVariantMetadataUpdated" }; + +export type ProductEventSubscriptionFragment_ProductVariantOutOfStock = { + productVariant: ProductEventSubscriptionFragment_ProductVariantOutOfStock_productVariant_ProductVariant | null; +} & { __typename: "ProductVariantOutOfStock" }; + +export type ProductEventSubscriptionFragment_ProductVariantStockUpdated = { + productVariant: ProductEventSubscriptionFragment_ProductVariantStockUpdated_productVariant_ProductVariant | null; +} & { __typename: "ProductVariantStockUpdated" }; + +export type ProductEventSubscriptionFragment_ProductVariantUpdated = { + productVariant: ProductEventSubscriptionFragment_ProductVariantUpdated_productVariant_ProductVariant | null; +} & { __typename: "ProductVariantUpdated" }; export type ProductEventSubscriptionFragment = | ProductEventSubscriptionFragment_Uchm3Qz7YjEsQhTMfPIk01DEzLiWluHMnX4k1L6Dt0s @@ -329,9 +1081,9 @@ export type ProductEventSubscriptionFragment = | ProductEventSubscriptionFragment_XWulXk1GqeHNvK2Zjg39D81UqhO8ZykBvc7wuJEvA | ProductEventSubscriptionFragment_Qo3grqPrpe4HInn1EwEhNaiRstQso5tTjYam1lLlKa | ProductEventSubscriptionFragment_0Hg7UwAf5qDqrfrBkEq72AxaObrB0w2l4xizB32wmho - | ProductEventSubscriptionFragment_GaRpV5WgLmyYwzvOncnidjvA9BqsuLaqkK8MskS5QAk - | ProductEventSubscriptionFragment_HwOxxNnVYeYl4jbk7puxZvo4KTvQJnkcex833Wfg - | ProductEventSubscriptionFragment_90suajkXt8hiTAxfKMvbNnr0Zjq2loXudhjMa1vc + | ProductEventSubscriptionFragment_8vnEs7OoSiff0XfWWmMcVlo7I5RldGpA4Gzu0Yf0 + | ProductEventSubscriptionFragment_UGfT5W4pN8hUpJmdIVnWUjEHvpe0Yu0P6Z3bGk3l2aY + | ProductEventSubscriptionFragment_NEkHKuNaPv7LMkfTm0zqQlkVvSmdwmdm3Po4HHoTe | ProductEventSubscriptionFragment_ProductDeleted | ProductEventSubscriptionFragment_ProductMediaCreated | ProductEventSubscriptionFragment_ProductMediaDeleted @@ -344,22 +1096,36 @@ export type ProductEventSubscriptionFragment = | ProductEventSubscriptionFragment_ProductVariantMetadataUpdated | ProductEventSubscriptionFragment_ProductVariantOutOfStock | ProductEventSubscriptionFragment_ProductVariantStockUpdated - | ProductEventSubscriptionFragment_ProductVariantUpdated -; + | ProductEventSubscriptionFragment_ProductVariantUpdated; -export type TaxedMoney_TaxedMoney_net_Money = { currency: string, amount: number }; +export type TaxedMoney_TaxedMoney_net_Money = { + currency: string; + amount: number; +}; -export type TaxedMoney_TaxedMoney_gross_Money = { currency: string, amount: number }; +export type TaxedMoney_TaxedMoney_gross_Money = { + currency: string; + amount: number; +}; -export type TaxedMoney_TaxedMoney_tax_Money = { currency: string, amount: number }; +export type TaxedMoney_TaxedMoney_tax_Money = { + currency: string; + amount: number; +}; -export type TaxedMoney = { net: TaxedMoney_TaxedMoney_net_Money, gross: TaxedMoney_TaxedMoney_gross_Money, tax: TaxedMoney_TaxedMoney_tax_Money }; +export type TaxedMoney = { + net: TaxedMoney_TaxedMoney_net_Money; + gross: TaxedMoney_TaxedMoney_gross_Money; + tax: TaxedMoney_TaxedMoney_tax_Money; +}; export class TypedDocumentString extends String implements DocumentTypeDecoration { - __apiType?: NonNullable['__apiType']>; + __apiType?: NonNullable< + DocumentTypeDecoration["__apiType"] + >; private value: string; public __meta__?: Record | undefined; @@ -373,7 +1139,8 @@ export class TypedDocumentString return this.value; } } -export const CollectionEventSubscriptionFragment = new TypedDocumentString(` +export const CollectionEventSubscriptionFragment = new TypedDocumentString( + ` fragment CollectionEventSubscriptionFragment on Event { __typename ... on CollectionUpdated { @@ -387,8 +1154,14 @@ export const CollectionEventSubscriptionFragment = new TypedDocumentString(` } } } - `, {"fragmentName":"CollectionEventSubscriptionFragment"}) as unknown as TypedDocumentString; -export const MenuEventSubscriptionFragment = new TypedDocumentString(` + `, + { fragmentName: "CollectionEventSubscriptionFragment" }, +) as unknown as TypedDocumentString< + CollectionEventSubscriptionFragment, + unknown +>; +export const MenuEventSubscriptionFragment = new TypedDocumentString( + ` fragment MenuEventSubscriptionFragment on Event { __typename ... on MenuCreated { @@ -428,8 +1201,11 @@ export const MenuEventSubscriptionFragment = new TypedDocumentString(` } } } - `, {"fragmentName":"MenuEventSubscriptionFragment"}) as unknown as TypedDocumentString; -export const PageEventSubscriptionFragment = new TypedDocumentString(` + `, + { fragmentName: "MenuEventSubscriptionFragment" }, +) as unknown as TypedDocumentString; +export const PageEventSubscriptionFragment = new TypedDocumentString( + ` fragment PageEventSubscriptionFragment on Event { __typename ... on PageCreated { @@ -463,8 +1239,11 @@ export const PageEventSubscriptionFragment = new TypedDocumentString(` } } } - `, {"fragmentName":"PageEventSubscriptionFragment"}) as unknown as TypedDocumentString; -export const ProductEventSubscriptionFragment = new TypedDocumentString(` + `, + { fragmentName: "PageEventSubscriptionFragment" }, +) as unknown as TypedDocumentString; +export const ProductEventSubscriptionFragment = new TypedDocumentString( + ` fragment ProductEventSubscriptionFragment on Event { __typename ... on ProductUpdated { @@ -547,8 +1326,11 @@ export const ProductEventSubscriptionFragment = new TypedDocumentString(` } } } - `, {"fragmentName":"ProductEventSubscriptionFragment"}) as unknown as TypedDocumentString; -export const TaxedMoney = new TypedDocumentString(` + `, + { fragmentName: "ProductEventSubscriptionFragment" }, +) as unknown as TypedDocumentString; +export const TaxedMoney = new TypedDocumentString( + ` fragment TaxedMoney on TaxedMoney { net { ...Money @@ -563,4 +1345,6 @@ export const TaxedMoney = new TypedDocumentString(` fragment Money on Money { currency amount -}`, {"fragmentName":"TaxedMoney"}) as unknown as TypedDocumentString; \ No newline at end of file +}`, + { fragmentName: "TaxedMoney" }, +) as unknown as TypedDocumentString; diff --git a/apps/stripe/src/graphql/generated/client.ts b/apps/stripe/src/graphql/generated/client.ts index 9540a2aa..f27ae090 100644 --- a/apps/stripe/src/graphql/generated/client.ts +++ b/apps/stripe/src/graphql/generated/client.ts @@ -22235,6 +22235,30 @@ export type ProductVariantDeletedProductVariantArgs = { channel?: InputMaybe; }; +/** + * Event sent when product variant discounted price is recalculated. + * + * Added in Saleor 3.22. + */ +export type ProductVariantDiscountedPriceUpdated = Event & { + /** The channel where the price changed. */ + channel: Channel; + /** Time of the event. */ + issuedAt: Maybe; + /** The user or application that triggered the event. */ + issuingPrincipal: Maybe; + /** The new discounted price. */ + newPrice: Money; + /** The previous discounted price. */ + previousPrice: Money; + /** The product variant the event relates to. */ + productVariant: ProductVariant; + /** The application receiving the webhook. */ + recipient: Maybe; + /** Saleor version that triggered the event. */ + version: Maybe; +}; + export type ProductVariantFilterInput = { isPreorder?: InputMaybe; metadata?: InputMaybe>; @@ -27242,6 +27266,14 @@ export type Subscription = { * Note: this API is currently in Feature Preview and can be subject to changes at later point. */ orderUpdated: Maybe; + /** + * Event sent when product variant discounted price is recalculated. + * + * Added in Saleor 3.22. + * + * Note: this API is currently in Feature Preview and can be subject to changes at later point. + */ + productVariantDiscountedPriceUpdated: Maybe; }; export type SubscriptionCheckoutCreatedArgs = { @@ -27324,6 +27356,10 @@ export type SubscriptionOrderUpdatedArgs = { channels?: InputMaybe>; }; +export type SubscriptionProductVariantDiscountedPriceUpdatedArgs = { + channels?: InputMaybe>; +}; + export type TaxCalculationStrategy = "FLAT_RATES" | "TAX_APP"; /** Tax class is a named object used to define tax rates per country. Tax class can be assigned to product types, products and shipping methods to define their tax rates. */ @@ -30599,6 +30635,7 @@ export type WebhookEventTypeAsyncEnum = | "PRODUCT_VARIANT_CREATED" /** A product variant is deleted. Warning: this event will not be executed when parent product has been deleted. Check PRODUCT_DELETED. */ | "PRODUCT_VARIANT_DELETED" + | "PRODUCT_VARIANT_DISCOUNTED_PRICE_UPDATED" /** A product variant metadata is updated. */ | "PRODUCT_VARIANT_METADATA_UPDATED" /** A product variant is out of stock. */ @@ -30924,6 +30961,7 @@ export type WebhookEventTypeEnum = | "PRODUCT_VARIANT_CREATED" /** A product variant is deleted. Warning: this event will not be executed when parent product has been deleted. Check PRODUCT_DELETED. */ | "PRODUCT_VARIANT_DELETED" + | "PRODUCT_VARIANT_DISCOUNTED_PRICE_UPDATED" /** A product variant metadata is updated. */ | "PRODUCT_VARIANT_METADATA_UPDATED" /** A product variant is out of stock. */ @@ -31165,6 +31203,7 @@ export type WebhookSampleEventTypeEnum = | "PRODUCT_VARIANT_BACK_IN_STOCK" | "PRODUCT_VARIANT_CREATED" | "PRODUCT_VARIANT_DELETED" + | "PRODUCT_VARIANT_DISCOUNTED_PRICE_UPDATED" | "PRODUCT_VARIANT_METADATA_UPDATED" | "PRODUCT_VARIANT_OUT_OF_STOCK" | "PRODUCT_VARIANT_STOCK_UPDATED" diff --git a/packages/codegen/schema.ts b/packages/codegen/schema.ts index a1f9e59e..94378d8b 100644 --- a/packages/codegen/schema.ts +++ b/packages/codegen/schema.ts @@ -22753,6 +22753,30 @@ export type ProductVariantDeletedProductVariantArgs = { channel?: InputMaybe; }; +/** + * Event sent when product variant discounted price is recalculated. + * + * Added in Saleor 3.22. + */ +export type ProductVariantDiscountedPriceUpdated = Event & { + /** The channel where the price changed. */ + channel: Channel; + /** Time of the event. */ + issuedAt: Maybe; + /** The user or application that triggered the event. */ + issuingPrincipal: Maybe; + /** The new discounted price. */ + newPrice: Money; + /** The previous discounted price. */ + previousPrice: Money; + /** The product variant the event relates to. */ + productVariant: ProductVariant; + /** The application receiving the webhook. */ + recipient: Maybe; + /** Saleor version that triggered the event. */ + version: Maybe; +}; + export type ProductVariantFilterInput = { isPreorder?: InputMaybe; metadata?: InputMaybe>; @@ -27898,6 +27922,14 @@ export type Subscription = { * Note: this API is currently in Feature Preview and can be subject to changes at later point. */ orderUpdated: Maybe; + /** + * Event sent when product variant discounted price is recalculated. + * + * Added in Saleor 3.22. + * + * Note: this API is currently in Feature Preview and can be subject to changes at later point. + */ + productVariantDiscountedPriceUpdated: Maybe; }; @@ -28000,6 +28032,11 @@ export type SubscriptionOrderUpdatedArgs = { channels?: InputMaybe>; }; + +export type SubscriptionProductVariantDiscountedPriceUpdatedArgs = { + channels?: InputMaybe>; +}; + export type TaxCalculationStrategy = | 'FLAT_RATES' | 'TAX_APP'; @@ -31320,6 +31357,7 @@ export type WebhookEventTypeAsyncEnum = | 'PRODUCT_VARIANT_CREATED' /** A product variant is deleted. Warning: this event will not be executed when parent product has been deleted. Check PRODUCT_DELETED. */ | 'PRODUCT_VARIANT_DELETED' + | 'PRODUCT_VARIANT_DISCOUNTED_PRICE_UPDATED' /** A product variant metadata is updated. */ | 'PRODUCT_VARIANT_METADATA_UPDATED' /** A product variant is out of stock. */ @@ -31645,6 +31683,7 @@ export type WebhookEventTypeEnum = | 'PRODUCT_VARIANT_CREATED' /** A product variant is deleted. Warning: this event will not be executed when parent product has been deleted. Check PRODUCT_DELETED. */ | 'PRODUCT_VARIANT_DELETED' + | 'PRODUCT_VARIANT_DISCOUNTED_PRICE_UPDATED' /** A product variant metadata is updated. */ | 'PRODUCT_VARIANT_METADATA_UPDATED' /** A product variant is out of stock. */ @@ -31886,6 +31925,7 @@ export type WebhookSampleEventTypeEnum = | 'PRODUCT_VARIANT_BACK_IN_STOCK' | 'PRODUCT_VARIANT_CREATED' | 'PRODUCT_VARIANT_DELETED' + | 'PRODUCT_VARIANT_DISCOUNTED_PRICE_UPDATED' | 'PRODUCT_VARIANT_METADATA_UPDATED' | 'PRODUCT_VARIANT_OUT_OF_STOCK' | 'PRODUCT_VARIANT_STOCK_UPDATED' diff --git a/packages/infrastructure/src/cms-page/saleor/graphql/queries/generated.ts b/packages/infrastructure/src/cms-page/saleor/graphql/queries/generated.ts index 5db64584..36252a31 100644 --- a/packages/infrastructure/src/cms-page/saleor/graphql/queries/generated.ts +++ b/packages/infrastructure/src/cms-page/saleor/graphql/queries/generated.ts @@ -28,32 +28,6 @@ export type PageVariables = Types.Exact<{ export type Page = Page_Query; -export type PagesBySlugs_pages_PageCountableConnection_edges_PageCountableEdge_node_Page_pageType_PageType = { slug: string }; - -export type PagesBySlugs_pages_PageCountableConnection_edges_PageCountableEdge_node_Page_attributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: Page_page_Page_attributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; - -export type PagesBySlugs_pages_PageCountableConnection_edges_PageCountableEdge_node_Page_attributes_SelectedAttribute_values_AttributeValue = { slug: string | null, name: string | null, plainText: string | null, richText: string | null, boolean: boolean | null, date: string | null, dateTime: string | null, reference: string | null, value: string | null, translation: Page_page_Page_attributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation | null, file: Page_page_Page_attributes_SelectedAttribute_values_AttributeValue_file_File | null }; - -export type PagesBySlugs_pages_PageCountableConnection_edges_PageCountableEdge_node_Page_attributes_SelectedAttribute = { attribute: PagesBySlugs_pages_PageCountableConnection_edges_PageCountableEdge_node_Page_attributes_SelectedAttribute_attribute_Attribute, values: Array }; - -export type PagesBySlugs_pages_PageCountableConnection_edges_PageCountableEdge_node_Page = { id: string, slug: string, title: string, content: string | null, pageType: PagesBySlugs_pages_PageCountableConnection_edges_PageCountableEdge_node_Page_pageType_PageType, attributes: Array }; - -export type PagesBySlugs_pages_PageCountableConnection_edges_PageCountableEdge = { node: PagesBySlugs_pages_PageCountableConnection_edges_PageCountableEdge_node_Page }; - -export type PagesBySlugs_pages_PageCountableConnection = { edges: Array }; - -export type PagesBySlugs_Query = { pages: PagesBySlugs_pages_PageCountableConnection | null }; - - -export type PagesBySlugsVariables = Types.Exact<{ - first: Types.Scalars['Int']['input']; - filter: Types.PageFilterInput; - languageCode: Types.LanguageCodeEnum; -}>; - - -export type PagesBySlugs = PagesBySlugs_Query; - export class TypedDocumentString extends String implements DocumentTypeDecoration @@ -118,55 +92,4 @@ fragment AttributeValueFragment on AttributeValue { file { url } -}`) as unknown as TypedDocumentString; -export const PagesBySlugsDocument = new TypedDocumentString(` - query PagesBySlugs($first: Int!, $filter: PageFilterInput!, $languageCode: LanguageCodeEnum!) { - pages(first: $first, filter: $filter) { - edges { - node { - id - slug - pageType { - slug - } - attributes { - attribute { - ...AttributeFragment - } - values { - ...AttributeValueFragment - } - } - title - content - } - } - } -} - fragment AttributeFragment on Attribute { - slug - inputType - name - translation(languageCode: $languageCode) { - name - } -} -fragment AttributeValueFragment on AttributeValue { - slug - name - plainText - richText - boolean - date - dateTime - reference - value - translation(languageCode: $languageCode) { - name - plainText - richText - } - file { - url - } -}`) as unknown as TypedDocumentString; \ No newline at end of file +}`) as unknown as TypedDocumentString; \ No newline at end of file diff --git a/packages/infrastructure/src/webhooks/saleor/graphql/fragments/generated.ts b/packages/infrastructure/src/webhooks/saleor/graphql/fragments/generated.ts index f4403b2c..0fa17245 100644 --- a/packages/infrastructure/src/webhooks/saleor/graphql/fragments/generated.ts +++ b/packages/infrastructure/src/webhooks/saleor/graphql/fragments/generated.ts @@ -15,11 +15,11 @@ export type CollectionEventSubscriptionFragment_NzAgx5ipNwNrprvvHc2qLsxAIqOZxfb0 export type CollectionEventSubscriptionFragment_1E3eTt7xP6B7Mkgmqq5X7sVIf5QtQseWfIxUecQwhV0 = { __typename: 'PageTypeCreated' | 'PageTypeDeleted' | 'PageTypeUpdated' | 'PageUpdated' | 'PaymentAuthorize' | 'PaymentCaptureEvent' | 'PaymentConfirmEvent' | 'PaymentGatewayInitializeSession' | 'PaymentGatewayInitializeTokenizationSession' | 'PaymentListGateways' | 'PaymentMethodInitializeTokenizationSession' | 'PaymentMethodProcessTokenizationSession' | 'PaymentProcessEvent' | 'PaymentRefundEvent' | 'PaymentVoidEvent' | 'PermissionGroupCreated' | 'PermissionGroupDeleted' | 'PermissionGroupUpdated' | 'ProductCreated' | 'ProductDeleted' }; -export type CollectionEventSubscriptionFragment_Z7rwgw8zu01Bnc5SgQqNwv0Lyp1jQ30oJz3Z8uvrhEw = { __typename: 'ProductExportCompleted' | 'ProductMediaCreated' | 'ProductMediaDeleted' | 'ProductMediaUpdated' | 'ProductMetadataUpdated' | 'ProductUpdated' | 'ProductVariantBackInStock' | 'ProductVariantCreated' | 'ProductVariantDeleted' | 'ProductVariantMetadataUpdated' | 'ProductVariantOutOfStock' | 'ProductVariantStockUpdated' | 'ProductVariantUpdated' | 'PromotionCreated' | 'PromotionDeleted' | 'PromotionEnded' | 'PromotionRuleCreated' | 'PromotionRuleDeleted' | 'PromotionRuleUpdated' | 'PromotionStarted' }; +export type CollectionEventSubscriptionFragment_VaNEf27jhQMii20zJvI0z7FmZ2UaHv3dIrHb6Xvvd4 = { __typename: 'ProductExportCompleted' | 'ProductMediaCreated' | 'ProductMediaDeleted' | 'ProductMediaUpdated' | 'ProductMetadataUpdated' | 'ProductUpdated' | 'ProductVariantBackInStock' | 'ProductVariantCreated' | 'ProductVariantDeleted' | 'ProductVariantDiscountedPriceUpdated' | 'ProductVariantMetadataUpdated' | 'ProductVariantOutOfStock' | 'ProductVariantStockUpdated' | 'ProductVariantUpdated' | 'PromotionCreated' | 'PromotionDeleted' | 'PromotionEnded' | 'PromotionRuleCreated' | 'PromotionRuleDeleted' | 'PromotionRuleUpdated' }; -export type CollectionEventSubscriptionFragment_FsCnoBef8jWxBeWLo55z4FkYvj19c9pK9ySq77aoQq = { __typename: 'PromotionUpdated' | 'SaleCreated' | 'SaleDeleted' | 'SaleToggle' | 'SaleUpdated' | 'ShippingListMethodsForCheckout' | 'ShippingPriceCreated' | 'ShippingPriceDeleted' | 'ShippingPriceUpdated' | 'ShippingZoneCreated' | 'ShippingZoneDeleted' | 'ShippingZoneMetadataUpdated' | 'ShippingZoneUpdated' | 'ShopMetadataUpdated' | 'StaffCreated' | 'StaffDeleted' | 'StaffSetPasswordRequested' | 'StaffUpdated' | 'StoredPaymentMethodDeleteRequested' | 'ThumbnailCreated' }; +export type CollectionEventSubscriptionFragment_Md8zs0I8HeprlIm0922DbYOqrfIxezBupbi88J9Cyo = { __typename: 'PromotionStarted' | 'PromotionUpdated' | 'SaleCreated' | 'SaleDeleted' | 'SaleToggle' | 'SaleUpdated' | 'ShippingListMethodsForCheckout' | 'ShippingPriceCreated' | 'ShippingPriceDeleted' | 'ShippingPriceUpdated' | 'ShippingZoneCreated' | 'ShippingZoneDeleted' | 'ShippingZoneMetadataUpdated' | 'ShippingZoneUpdated' | 'ShopMetadataUpdated' | 'StaffCreated' | 'StaffDeleted' | 'StaffSetPasswordRequested' | 'StaffUpdated' | 'StoredPaymentMethodDeleteRequested' }; -export type CollectionEventSubscriptionFragment_V8I7ofYZlQ6mRbyZppdZBtAVuVhCxrv4cl7r3Nc1xQ = { __typename: 'TransactionCancelationRequested' | 'TransactionChargeRequested' | 'TransactionInitializeSession' | 'TransactionItemMetadataUpdated' | 'TransactionProcessSession' | 'TransactionRefundRequested' | 'TranslationCreated' | 'TranslationUpdated' | 'VoucherCodeExportCompleted' | 'VoucherCodesCreated' | 'VoucherCodesDeleted' | 'VoucherCreated' | 'VoucherDeleted' | 'VoucherMetadataUpdated' | 'VoucherUpdated' | 'WarehouseCreated' | 'WarehouseDeleted' | 'WarehouseMetadataUpdated' | 'WarehouseUpdated' }; +export type CollectionEventSubscriptionFragment_Dp5dDbTdpxGfQktwrh3Py85k1XyXjuGj32jhqJ1ZhSs = { __typename: 'ThumbnailCreated' | 'TransactionCancelationRequested' | 'TransactionChargeRequested' | 'TransactionInitializeSession' | 'TransactionItemMetadataUpdated' | 'TransactionProcessSession' | 'TransactionRefundRequested' | 'TranslationCreated' | 'TranslationUpdated' | 'VoucherCodeExportCompleted' | 'VoucherCodesCreated' | 'VoucherCodesDeleted' | 'VoucherCreated' | 'VoucherDeleted' | 'VoucherMetadataUpdated' | 'VoucherUpdated' | 'WarehouseCreated' | 'WarehouseDeleted' | 'WarehouseMetadataUpdated' | 'WarehouseUpdated' }; export type CollectionEventSubscriptionFragment_CollectionDeleted = ( { collection: CollectionEventSubscriptionFragment_CollectionDeleted_collection_Collection | null } @@ -37,9 +37,9 @@ export type CollectionEventSubscriptionFragment = | CollectionEventSubscriptionFragment_IOmIHgezj4BqSe0qBa27Ry4w4In3hD62xLNv1Dlw | CollectionEventSubscriptionFragment_NzAgx5ipNwNrprvvHc2qLsxAIqOZxfb0Ab5nlcQwU | CollectionEventSubscriptionFragment_1E3eTt7xP6B7Mkgmqq5X7sVIf5QtQseWfIxUecQwhV0 - | CollectionEventSubscriptionFragment_Z7rwgw8zu01Bnc5SgQqNwv0Lyp1jQ30oJz3Z8uvrhEw - | CollectionEventSubscriptionFragment_FsCnoBef8jWxBeWLo55z4FkYvj19c9pK9ySq77aoQq - | CollectionEventSubscriptionFragment_V8I7ofYZlQ6mRbyZppdZBtAVuVhCxrv4cl7r3Nc1xQ + | CollectionEventSubscriptionFragment_VaNEf27jhQMii20zJvI0z7FmZ2UaHv3dIrHb6Xvvd4 + | CollectionEventSubscriptionFragment_Md8zs0I8HeprlIm0922DbYOqrfIxezBupbi88J9Cyo + | CollectionEventSubscriptionFragment_Dp5dDbTdpxGfQktwrh3Py85k1XyXjuGj32jhqJ1ZhSs | CollectionEventSubscriptionFragment_CollectionDeleted | CollectionEventSubscriptionFragment_CollectionUpdated ; @@ -72,11 +72,11 @@ export type MenuEventSubscriptionFragment_6SZv9znezpLhGpS69dQ1GbY5yDKa5XUxykJztq export type MenuEventSubscriptionFragment_6kRlk3To6sPpW0QQr52mUNjVn2CzSdpyN0o8Cy5kQ70 = { __typename: 'PaymentAuthorize' | 'PaymentCaptureEvent' | 'PaymentConfirmEvent' | 'PaymentGatewayInitializeSession' | 'PaymentGatewayInitializeTokenizationSession' | 'PaymentListGateways' | 'PaymentMethodInitializeTokenizationSession' | 'PaymentMethodProcessTokenizationSession' | 'PaymentProcessEvent' | 'PaymentRefundEvent' | 'PaymentVoidEvent' | 'PermissionGroupCreated' | 'PermissionGroupDeleted' | 'PermissionGroupUpdated' | 'ProductCreated' | 'ProductDeleted' | 'ProductExportCompleted' | 'ProductMediaCreated' | 'ProductMediaDeleted' | 'ProductMediaUpdated' }; -export type MenuEventSubscriptionFragment_Rhl3UnNk6nEi747MfPtEghacMvrlEq0zkU4BVjs = { __typename: 'ProductMetadataUpdated' | 'ProductUpdated' | 'ProductVariantBackInStock' | 'ProductVariantCreated' | 'ProductVariantDeleted' | 'ProductVariantMetadataUpdated' | 'ProductVariantOutOfStock' | 'ProductVariantStockUpdated' | 'ProductVariantUpdated' | 'PromotionCreated' | 'PromotionDeleted' | 'PromotionEnded' | 'PromotionRuleCreated' | 'PromotionRuleDeleted' | 'PromotionRuleUpdated' | 'PromotionStarted' | 'PromotionUpdated' | 'SaleCreated' | 'SaleDeleted' | 'SaleToggle' }; +export type MenuEventSubscriptionFragment_Vj0rLuHjNAcQ6LqPpibXk9LyMpl7ObVs0GDdoBpJh4 = { __typename: 'ProductMetadataUpdated' | 'ProductUpdated' | 'ProductVariantBackInStock' | 'ProductVariantCreated' | 'ProductVariantDeleted' | 'ProductVariantDiscountedPriceUpdated' | 'ProductVariantMetadataUpdated' | 'ProductVariantOutOfStock' | 'ProductVariantStockUpdated' | 'ProductVariantUpdated' | 'PromotionCreated' | 'PromotionDeleted' | 'PromotionEnded' | 'PromotionRuleCreated' | 'PromotionRuleDeleted' | 'PromotionRuleUpdated' | 'PromotionStarted' | 'PromotionUpdated' | 'SaleCreated' | 'SaleDeleted' }; -export type MenuEventSubscriptionFragment_W5T9u8Ze80BUkN79Oq6SmIzz96pnf5x7CafMXoKzjye = { __typename: 'SaleUpdated' | 'ShippingListMethodsForCheckout' | 'ShippingPriceCreated' | 'ShippingPriceDeleted' | 'ShippingPriceUpdated' | 'ShippingZoneCreated' | 'ShippingZoneDeleted' | 'ShippingZoneMetadataUpdated' | 'ShippingZoneUpdated' | 'ShopMetadataUpdated' | 'StaffCreated' | 'StaffDeleted' | 'StaffSetPasswordRequested' | 'StaffUpdated' | 'StoredPaymentMethodDeleteRequested' | 'ThumbnailCreated' | 'TransactionCancelationRequested' | 'TransactionChargeRequested' | 'TransactionInitializeSession' | 'TransactionItemMetadataUpdated' }; +export type MenuEventSubscriptionFragment_6CiRoIuh4Yp4Dw8YuZkIoAi7nacE1LspcEvEinZSlh0 = { __typename: 'SaleToggle' | 'SaleUpdated' | 'ShippingListMethodsForCheckout' | 'ShippingPriceCreated' | 'ShippingPriceDeleted' | 'ShippingPriceUpdated' | 'ShippingZoneCreated' | 'ShippingZoneDeleted' | 'ShippingZoneMetadataUpdated' | 'ShippingZoneUpdated' | 'ShopMetadataUpdated' | 'StaffCreated' | 'StaffDeleted' | 'StaffSetPasswordRequested' | 'StaffUpdated' | 'StoredPaymentMethodDeleteRequested' | 'ThumbnailCreated' | 'TransactionCancelationRequested' | 'TransactionChargeRequested' | 'TransactionInitializeSession' }; -export type MenuEventSubscriptionFragment_YvQxrNkoOvnbjiQk7JSzAwJglAbMml79KptInbPmJ50 = { __typename: 'TransactionProcessSession' | 'TransactionRefundRequested' | 'TranslationCreated' | 'TranslationUpdated' | 'VoucherCodeExportCompleted' | 'VoucherCodesCreated' | 'VoucherCodesDeleted' | 'VoucherCreated' | 'VoucherDeleted' | 'VoucherMetadataUpdated' | 'VoucherUpdated' | 'WarehouseCreated' | 'WarehouseDeleted' | 'WarehouseMetadataUpdated' | 'WarehouseUpdated' }; +export type MenuEventSubscriptionFragment_FzRpvXbZsLUnyQcwg2iTzgAMy1kmkcQ76jDvP0ZVoA = { __typename: 'TransactionItemMetadataUpdated' | 'TransactionProcessSession' | 'TransactionRefundRequested' | 'TranslationCreated' | 'TranslationUpdated' | 'VoucherCodeExportCompleted' | 'VoucherCodesCreated' | 'VoucherCodesDeleted' | 'VoucherCreated' | 'VoucherDeleted' | 'VoucherMetadataUpdated' | 'VoucherUpdated' | 'WarehouseCreated' | 'WarehouseDeleted' | 'WarehouseMetadataUpdated' | 'WarehouseUpdated' }; export type MenuEventSubscriptionFragment_MenuCreated = ( { menu: MenuEventSubscriptionFragment_MenuCreated_menu_Menu | null } @@ -114,9 +114,9 @@ export type MenuEventSubscriptionFragment = | MenuEventSubscriptionFragment_XWulXk1GqeHNvK2Zjg39D81UqhO8ZykBvc7wuJEvA | MenuEventSubscriptionFragment_6SZv9znezpLhGpS69dQ1GbY5yDKa5XUxykJztqqTg6U | MenuEventSubscriptionFragment_6kRlk3To6sPpW0QQr52mUNjVn2CzSdpyN0o8Cy5kQ70 - | MenuEventSubscriptionFragment_Rhl3UnNk6nEi747MfPtEghacMvrlEq0zkU4BVjs - | MenuEventSubscriptionFragment_W5T9u8Ze80BUkN79Oq6SmIzz96pnf5x7CafMXoKzjye - | MenuEventSubscriptionFragment_YvQxrNkoOvnbjiQk7JSzAwJglAbMml79KptInbPmJ50 + | MenuEventSubscriptionFragment_Vj0rLuHjNAcQ6LqPpibXk9LyMpl7ObVs0GDdoBpJh4 + | MenuEventSubscriptionFragment_6CiRoIuh4Yp4Dw8YuZkIoAi7nacE1LspcEvEinZSlh0 + | MenuEventSubscriptionFragment_FzRpvXbZsLUnyQcwg2iTzgAMy1kmkcQ76jDvP0ZVoA | MenuEventSubscriptionFragment_MenuCreated | MenuEventSubscriptionFragment_MenuDeleted | MenuEventSubscriptionFragment_MenuItemCreated @@ -149,11 +149,11 @@ export type PageEventSubscriptionFragment_Qo3grqPrpe4HInn1EwEhNaiRstQso5tTjYam1l export type PageEventSubscriptionFragment_6kRlk3To6sPpW0QQr52mUNjVn2CzSdpyN0o8Cy5kQ70 = { __typename: 'PaymentAuthorize' | 'PaymentCaptureEvent' | 'PaymentConfirmEvent' | 'PaymentGatewayInitializeSession' | 'PaymentGatewayInitializeTokenizationSession' | 'PaymentListGateways' | 'PaymentMethodInitializeTokenizationSession' | 'PaymentMethodProcessTokenizationSession' | 'PaymentProcessEvent' | 'PaymentRefundEvent' | 'PaymentVoidEvent' | 'PermissionGroupCreated' | 'PermissionGroupDeleted' | 'PermissionGroupUpdated' | 'ProductCreated' | 'ProductDeleted' | 'ProductExportCompleted' | 'ProductMediaCreated' | 'ProductMediaDeleted' | 'ProductMediaUpdated' }; -export type PageEventSubscriptionFragment_Rhl3UnNk6nEi747MfPtEghacMvrlEq0zkU4BVjs = { __typename: 'ProductMetadataUpdated' | 'ProductUpdated' | 'ProductVariantBackInStock' | 'ProductVariantCreated' | 'ProductVariantDeleted' | 'ProductVariantMetadataUpdated' | 'ProductVariantOutOfStock' | 'ProductVariantStockUpdated' | 'ProductVariantUpdated' | 'PromotionCreated' | 'PromotionDeleted' | 'PromotionEnded' | 'PromotionRuleCreated' | 'PromotionRuleDeleted' | 'PromotionRuleUpdated' | 'PromotionStarted' | 'PromotionUpdated' | 'SaleCreated' | 'SaleDeleted' | 'SaleToggle' }; +export type PageEventSubscriptionFragment_Vj0rLuHjNAcQ6LqPpibXk9LyMpl7ObVs0GDdoBpJh4 = { __typename: 'ProductMetadataUpdated' | 'ProductUpdated' | 'ProductVariantBackInStock' | 'ProductVariantCreated' | 'ProductVariantDeleted' | 'ProductVariantDiscountedPriceUpdated' | 'ProductVariantMetadataUpdated' | 'ProductVariantOutOfStock' | 'ProductVariantStockUpdated' | 'ProductVariantUpdated' | 'PromotionCreated' | 'PromotionDeleted' | 'PromotionEnded' | 'PromotionRuleCreated' | 'PromotionRuleDeleted' | 'PromotionRuleUpdated' | 'PromotionStarted' | 'PromotionUpdated' | 'SaleCreated' | 'SaleDeleted' }; -export type PageEventSubscriptionFragment_W5T9u8Ze80BUkN79Oq6SmIzz96pnf5x7CafMXoKzjye = { __typename: 'SaleUpdated' | 'ShippingListMethodsForCheckout' | 'ShippingPriceCreated' | 'ShippingPriceDeleted' | 'ShippingPriceUpdated' | 'ShippingZoneCreated' | 'ShippingZoneDeleted' | 'ShippingZoneMetadataUpdated' | 'ShippingZoneUpdated' | 'ShopMetadataUpdated' | 'StaffCreated' | 'StaffDeleted' | 'StaffSetPasswordRequested' | 'StaffUpdated' | 'StoredPaymentMethodDeleteRequested' | 'ThumbnailCreated' | 'TransactionCancelationRequested' | 'TransactionChargeRequested' | 'TransactionInitializeSession' | 'TransactionItemMetadataUpdated' }; +export type PageEventSubscriptionFragment_6CiRoIuh4Yp4Dw8YuZkIoAi7nacE1LspcEvEinZSlh0 = { __typename: 'SaleToggle' | 'SaleUpdated' | 'ShippingListMethodsForCheckout' | 'ShippingPriceCreated' | 'ShippingPriceDeleted' | 'ShippingPriceUpdated' | 'ShippingZoneCreated' | 'ShippingZoneDeleted' | 'ShippingZoneMetadataUpdated' | 'ShippingZoneUpdated' | 'ShopMetadataUpdated' | 'StaffCreated' | 'StaffDeleted' | 'StaffSetPasswordRequested' | 'StaffUpdated' | 'StoredPaymentMethodDeleteRequested' | 'ThumbnailCreated' | 'TransactionCancelationRequested' | 'TransactionChargeRequested' | 'TransactionInitializeSession' }; -export type PageEventSubscriptionFragment_YvQxrNkoOvnbjiQk7JSzAwJglAbMml79KptInbPmJ50 = { __typename: 'TransactionProcessSession' | 'TransactionRefundRequested' | 'TranslationCreated' | 'TranslationUpdated' | 'VoucherCodeExportCompleted' | 'VoucherCodesCreated' | 'VoucherCodesDeleted' | 'VoucherCreated' | 'VoucherDeleted' | 'VoucherMetadataUpdated' | 'VoucherUpdated' | 'WarehouseCreated' | 'WarehouseDeleted' | 'WarehouseMetadataUpdated' | 'WarehouseUpdated' }; +export type PageEventSubscriptionFragment_FzRpvXbZsLUnyQcwg2iTzgAMy1kmkcQ76jDvP0ZVoA = { __typename: 'TransactionItemMetadataUpdated' | 'TransactionProcessSession' | 'TransactionRefundRequested' | 'TranslationCreated' | 'TranslationUpdated' | 'VoucherCodeExportCompleted' | 'VoucherCodesCreated' | 'VoucherCodesDeleted' | 'VoucherCreated' | 'VoucherDeleted' | 'VoucherMetadataUpdated' | 'VoucherUpdated' | 'WarehouseCreated' | 'WarehouseDeleted' | 'WarehouseMetadataUpdated' | 'WarehouseUpdated' }; export type PageEventSubscriptionFragment_PageCreated = ( { page: PageEventSubscriptionFragment_PageCreated_page_Page | null } @@ -191,9 +191,9 @@ export type PageEventSubscriptionFragment = | PageEventSubscriptionFragment_XWulXk1GqeHNvK2Zjg39D81UqhO8ZykBvc7wuJEvA | PageEventSubscriptionFragment_Qo3grqPrpe4HInn1EwEhNaiRstQso5tTjYam1lLlKa | PageEventSubscriptionFragment_6kRlk3To6sPpW0QQr52mUNjVn2CzSdpyN0o8Cy5kQ70 - | PageEventSubscriptionFragment_Rhl3UnNk6nEi747MfPtEghacMvrlEq0zkU4BVjs - | PageEventSubscriptionFragment_W5T9u8Ze80BUkN79Oq6SmIzz96pnf5x7CafMXoKzjye - | PageEventSubscriptionFragment_YvQxrNkoOvnbjiQk7JSzAwJglAbMml79KptInbPmJ50 + | PageEventSubscriptionFragment_Vj0rLuHjNAcQ6LqPpibXk9LyMpl7ObVs0GDdoBpJh4 + | PageEventSubscriptionFragment_6CiRoIuh4Yp4Dw8YuZkIoAi7nacE1LspcEvEinZSlh0 + | PageEventSubscriptionFragment_FzRpvXbZsLUnyQcwg2iTzgAMy1kmkcQ76jDvP0ZVoA | PageEventSubscriptionFragment_PageCreated | PageEventSubscriptionFragment_PageDeleted | PageEventSubscriptionFragment_PageTypeCreated @@ -252,11 +252,11 @@ export type ProductEventSubscriptionFragment_Qo3grqPrpe4HInn1EwEhNaiRstQso5tTjYa export type ProductEventSubscriptionFragment_0Hg7UwAf5qDqrfrBkEq72AxaObrB0w2l4xizB32wmho = { __typename: 'PageCreated' | 'PageDeleted' | 'PageTypeCreated' | 'PageTypeDeleted' | 'PageTypeUpdated' | 'PageUpdated' | 'PaymentAuthorize' | 'PaymentCaptureEvent' | 'PaymentConfirmEvent' | 'PaymentGatewayInitializeSession' | 'PaymentGatewayInitializeTokenizationSession' | 'PaymentListGateways' | 'PaymentMethodInitializeTokenizationSession' | 'PaymentMethodProcessTokenizationSession' | 'PaymentProcessEvent' | 'PaymentRefundEvent' | 'PaymentVoidEvent' | 'PermissionGroupCreated' | 'PermissionGroupDeleted' | 'PermissionGroupUpdated' }; -export type ProductEventSubscriptionFragment_GaRpV5WgLmyYwzvOncnidjvA9BqsuLaqkK8MskS5QAk = { __typename: 'ProductCreated' | 'ProductExportCompleted' | 'PromotionCreated' | 'PromotionDeleted' | 'PromotionEnded' | 'PromotionRuleCreated' | 'PromotionRuleDeleted' | 'PromotionRuleUpdated' | 'PromotionStarted' | 'PromotionUpdated' | 'SaleCreated' | 'SaleDeleted' | 'SaleToggle' | 'SaleUpdated' | 'ShippingListMethodsForCheckout' | 'ShippingPriceCreated' | 'ShippingPriceDeleted' | 'ShippingPriceUpdated' | 'ShippingZoneCreated' | 'ShippingZoneDeleted' }; +export type ProductEventSubscriptionFragment_8vnEs7OoSiff0XfWWmMcVlo7I5RldGpA4Gzu0Yf0 = { __typename: 'ProductCreated' | 'ProductExportCompleted' | 'ProductVariantDiscountedPriceUpdated' | 'PromotionCreated' | 'PromotionDeleted' | 'PromotionEnded' | 'PromotionRuleCreated' | 'PromotionRuleDeleted' | 'PromotionRuleUpdated' | 'PromotionStarted' | 'PromotionUpdated' | 'SaleCreated' | 'SaleDeleted' | 'SaleToggle' | 'SaleUpdated' | 'ShippingListMethodsForCheckout' | 'ShippingPriceCreated' | 'ShippingPriceDeleted' | 'ShippingPriceUpdated' | 'ShippingZoneCreated' }; -export type ProductEventSubscriptionFragment_HwOxxNnVYeYl4jbk7puxZvo4KTvQJnkcex833Wfg = { __typename: 'ShippingZoneMetadataUpdated' | 'ShippingZoneUpdated' | 'ShopMetadataUpdated' | 'StaffCreated' | 'StaffDeleted' | 'StaffSetPasswordRequested' | 'StaffUpdated' | 'StoredPaymentMethodDeleteRequested' | 'ThumbnailCreated' | 'TransactionCancelationRequested' | 'TransactionChargeRequested' | 'TransactionInitializeSession' | 'TransactionItemMetadataUpdated' | 'TransactionProcessSession' | 'TransactionRefundRequested' | 'TranslationCreated' | 'TranslationUpdated' | 'VoucherCodeExportCompleted' | 'VoucherCodesCreated' | 'VoucherCodesDeleted' }; +export type ProductEventSubscriptionFragment_UGfT5W4pN8hUpJmdIVnWUjEHvpe0Yu0P6Z3bGk3l2aY = { __typename: 'ShippingZoneDeleted' | 'ShippingZoneMetadataUpdated' | 'ShippingZoneUpdated' | 'ShopMetadataUpdated' | 'StaffCreated' | 'StaffDeleted' | 'StaffSetPasswordRequested' | 'StaffUpdated' | 'StoredPaymentMethodDeleteRequested' | 'ThumbnailCreated' | 'TransactionCancelationRequested' | 'TransactionChargeRequested' | 'TransactionInitializeSession' | 'TransactionItemMetadataUpdated' | 'TransactionProcessSession' | 'TransactionRefundRequested' | 'TranslationCreated' | 'TranslationUpdated' | 'VoucherCodeExportCompleted' | 'VoucherCodesCreated' }; -export type ProductEventSubscriptionFragment_90suajkXt8hiTAxfKMvbNnr0Zjq2loXudhjMa1vc = { __typename: 'VoucherCreated' | 'VoucherDeleted' | 'VoucherMetadataUpdated' | 'VoucherUpdated' | 'WarehouseCreated' | 'WarehouseDeleted' | 'WarehouseMetadataUpdated' | 'WarehouseUpdated' }; +export type ProductEventSubscriptionFragment_NEkHKuNaPv7LMkfTm0zqQlkVvSmdwmdm3Po4HHoTe = { __typename: 'VoucherCodesDeleted' | 'VoucherCreated' | 'VoucherDeleted' | 'VoucherMetadataUpdated' | 'VoucherUpdated' | 'WarehouseCreated' | 'WarehouseDeleted' | 'WarehouseMetadataUpdated' | 'WarehouseUpdated' }; export type ProductEventSubscriptionFragment_ProductDeleted = ( { product: ProductEventSubscriptionFragment_ProductDeleted_product_Product | null } @@ -329,9 +329,9 @@ export type ProductEventSubscriptionFragment = | ProductEventSubscriptionFragment_XWulXk1GqeHNvK2Zjg39D81UqhO8ZykBvc7wuJEvA | ProductEventSubscriptionFragment_Qo3grqPrpe4HInn1EwEhNaiRstQso5tTjYam1lLlKa | ProductEventSubscriptionFragment_0Hg7UwAf5qDqrfrBkEq72AxaObrB0w2l4xizB32wmho - | ProductEventSubscriptionFragment_GaRpV5WgLmyYwzvOncnidjvA9BqsuLaqkK8MskS5QAk - | ProductEventSubscriptionFragment_HwOxxNnVYeYl4jbk7puxZvo4KTvQJnkcex833Wfg - | ProductEventSubscriptionFragment_90suajkXt8hiTAxfKMvbNnr0Zjq2loXudhjMa1vc + | ProductEventSubscriptionFragment_8vnEs7OoSiff0XfWWmMcVlo7I5RldGpA4Gzu0Yf0 + | ProductEventSubscriptionFragment_UGfT5W4pN8hUpJmdIVnWUjEHvpe0Yu0P6Z3bGk3l2aY + | ProductEventSubscriptionFragment_NEkHKuNaPv7LMkfTm0zqQlkVvSmdwmdm3Po4HHoTe | ProductEventSubscriptionFragment_ProductDeleted | ProductEventSubscriptionFragment_ProductMediaCreated | ProductEventSubscriptionFragment_ProductMediaDeleted From 1f9dbc44662c22689e3db9cedb401055e8b5e166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bogda=C5=82?= Date: Mon, 13 Apr 2026 14:50:30 +0200 Subject: [PATCH 13/23] feat: adjust types and functionality to create multi-ventor orders --- .../app/api/payments/stripe/webhooks/route.ts | 80 +- .../src/graphql/generated/client.ts | 809 +++++++----------- apps/marketplace/src/lib/stripe/client.ts | 2 - .../src/lib/stripe/webhook-signature.ts | 69 +- .../[locale]/(checkout)/checkout/error.tsx | 15 + .../order/confirmation/[id]/actions.ts | 14 - .../[id]/components/checkout-remover.tsx | 35 - .../order/confirmation/[id]/page.tsx | 8 +- .../src/app/[locale]/(main)/error.tsx | 15 + .../actions/update-checkout-address-action.ts | 10 +- .../order-placed-cleanup-middleware.ts | 49 ++ .../checkout/sections/payment/actions.ts | 15 +- .../checkout/sections/payment/payment.tsx | 3 + .../is-transient-rsc-navigation-error.ts | 11 + .../rsc-stream-interrupted-fallback.tsx | 12 + apps/storefront/src/proxy.ts | 7 +- apps/stripe/src/graphql/generated/client.ts | 789 +++++++---------- packages/codegen/schema.ts | 795 +++++++---------- .../acp/saleor/graphql/fragments/generated.ts | 6 +- .../acp/saleor/graphql/mutations/generated.ts | 2 +- .../acp/saleor/graphql/queries/generated.ts | 6 +- .../saleor/graphql/fragments/generated.ts | 2 +- .../cart/saleor/graphql/queries/generated.ts | 2 +- .../saleor/graphql/fragments/generated.ts | 5 +- .../saleor/graphql/queries/generated.ts | 2 +- .../saleor/graphql/fragments/generated.ts | 2 +- .../saleor/graphql/queries/generated.ts | 2 +- .../saleor/graphql/queries/generated.ts | 2 +- .../src/graphql/fragments/generated.ts | 2 +- .../src/graphql/mutations/generated.ts | 8 +- .../saleor/graphql/queries/generated.ts | 2 +- .../saleor/graphql/fragments/generated.ts | 6 +- .../store/saleor/graphql/queries/generated.ts | 6 +- .../saleor/graphql/fragments/generated.ts | 2 +- .../user/saleor/graphql/queries/generated.ts | 2 +- 35 files changed, 1250 insertions(+), 1547 deletions(-) delete mode 100644 apps/storefront/src/app/[locale]/(checkout)/order/confirmation/[id]/actions.ts delete mode 100644 apps/storefront/src/app/[locale]/(checkout)/order/confirmation/[id]/components/checkout-remover.tsx create mode 100644 apps/storefront/src/foundation/checkout/order-placed-cleanup-middleware.ts create mode 100644 apps/storefront/src/foundation/errors/is-transient-rsc-navigation-error.ts create mode 100644 apps/storefront/src/foundation/errors/rsc-stream-interrupted-fallback.tsx diff --git a/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts b/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts index bee92b4e..81fbc26f 100644 --- a/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts +++ b/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts @@ -2,7 +2,7 @@ import { type NextRequest, NextResponse } from "next/server"; import type { TransactionCreateMutationVariables } from "@/graphql/generated/client"; import { getAppConfig } from "@/lib/saleor/app-config"; -import { verifyStripeWebhookSignature } from "@/lib/stripe/webhook-signature"; +import { verifyStripeWebhookSignatureDetailed } from "@/lib/stripe/webhook-signature"; import { checkoutService } from "@/services/checkouts"; import { marketplaceLogger } from "@/services/logging"; import { transactionsService } from "@/services/transactions"; @@ -54,6 +54,19 @@ type CheckoutCompletePayload = { } | null; }; +/** + * Include `debug` in JSON error responses (visible in Stripe Dashboard → webhook delivery). + * Opt-in only: set `STRIPE_WEBHOOK_DEBUG=true` or `1`. Otherwise never attached. + */ +const stripeWebhookDebugInResponse = (): boolean => { + const v = process.env.STRIPE_WEBHOOK_DEBUG?.toLowerCase(); + + return v === "true" || v === "1"; +}; + +const truncateForLog = (text: string, maxChars: number): string => + text.length <= maxChars ? text : `${text.slice(0, maxChars)}…[truncated]`; + const isCheckoutCompleteNotFoundError = (code: string) => code === "NOT_FOUND"; const mapCheckoutCompleteErrors = ( @@ -92,23 +105,59 @@ const getSaleorDomainFromEnv = () => { export async function POST(request: NextRequest) { const stripeSignature = request.headers.get("stripe-signature"); + const rawPayload = await request.text(); + + const baseDebug = { + bodyByteLength: Buffer.byteLength(rawPayload, "utf8"), + hasStripeSignatureHeader: Boolean(stripeSignature), + stripeSignatureHeaderLength: stripeSignature?.length ?? 0, + hasWebhookSigningSecret: Boolean(process.env.STRIPE_WEBHOOK_SIGNING_SECRET), + }; if (!stripeSignature) { + const debug = { + ...baseDebug, + step: "missing_stripe_signature_header" as const, + }; + + marketplaceLogger.error( + "Stripe webhook rejected: missing stripe-signature", + { + ...debug, + rawBodyPreview: truncateForLog(rawPayload, 600), + }, + ); + return NextResponse.json( - { error: "Missing stripe-signature header." }, + stripeWebhookDebugInResponse() + ? { error: "Missing stripe-signature header.", debug } + : { error: "Missing stripe-signature header." }, { status: 400 }, ); } - const rawPayload = await request.text(); - const isValidSignature = verifyStripeWebhookSignature({ + const signatureResult = verifyStripeWebhookSignatureDetailed({ payload: rawPayload, stripeSignature, }); - if (!isValidSignature) { + if (!signatureResult.ok) { + const debug = { + ...baseDebug, + step: "signature_verification_failed" as const, + verifyReason: signatureResult.reason, + clockSkewSeconds: signatureResult.clockSkewSeconds, + }; + + marketplaceLogger.error("Stripe webhook rejected: invalid signature", { + ...debug, + rawBodyPreview: truncateForLog(rawPayload, 600), + }); + return NextResponse.json( - { error: "Invalid Stripe webhook signature." }, + stripeWebhookDebugInResponse() + ? { error: "Invalid Stripe webhook signature.", debug } + : { error: "Invalid Stripe webhook signature." }, { status: 400 }, ); } @@ -117,9 +166,24 @@ export async function POST(request: NextRequest) { try { event = JSON.parse(rawPayload) as StripePaymentIntentSucceededEvent; - } catch { + } catch (parseError) { + const debug = { + ...baseDebug, + step: "json_parse_failed" as const, + parseError: + parseError instanceof Error ? parseError.message : String(parseError), + rawBodyPreview: truncateForLog(rawPayload, 800), + }; + + marketplaceLogger.error( + "Stripe webhook rejected: JSON parse failed", + debug, + ); + return NextResponse.json( - { error: "Invalid Stripe payload." }, + stripeWebhookDebugInResponse() + ? { error: "Invalid Stripe payload.", debug } + : { error: "Invalid Stripe payload." }, { status: 400 }, ); } diff --git a/apps/marketplace/src/graphql/generated/client.ts b/apps/marketplace/src/graphql/generated/client.ts index a6daaba4..bd80286b 100644 --- a/apps/marketplace/src/graphql/generated/client.ts +++ b/apps/marketplace/src/graphql/generated/client.ts @@ -255,6 +255,7 @@ export type AccountErrorCode = | 'DELETE_OWN_ACCOUNT' | 'DELETE_STAFF_ACCOUNT' | 'DELETE_SUPERUSER_ACCOUNT' + | 'DISABLED_AUTHENTICATION_METHOD' | 'DUPLICATED_INPUT_ITEM' | 'GRAPHQL_ERROR' | 'INACTIVE' @@ -1045,38 +1046,21 @@ export type AppExtension = Node & { /** Label of the extension to show in the dashboard. */ label: Scalars['String']['output']; /** - * Place where given extension will be mounted. - * @deprecated Use `mountName` instead. - */ - mount: AppExtensionMountEnum; - /** - * Name of the extension mount point in the dashboard. Replaces `mount` + * Name of the extension mount point in the dashboard. Value returned in UPPERCASE. * * Added in Saleor 3.22. */ mountName: Scalars['String']['output']; - /** - * App extension options. - * - * Added in Saleor 3.22. - * @deprecated Use `settings` field instead. - */ - options: Maybe; /** List of the app extension's permissions. */ permissions: Array; /** - * App extension settings. Replaces `options` field. + * App extension settings. * * Added in Saleor 3.22. */ settings: Scalars['JSON']['output']; /** - * Type of way how app extension will be opened. - * @deprecated Use `targetName` instead. - */ - target: AppExtensionTargetEnum; - /** - * Name of the extension target in the dashboard. Replaces `target` + * Name of the extension target in the dashboard. Value returned in UPPERCASE. * * Added in Saleor 3.22. */ @@ -1101,24 +1085,12 @@ export type AppExtensionCountableEdge = { }; export type AppExtensionFilterInput = { - /** - * DEPRECATED: Use `mountName` instead. - * - * DEPRECATED: this field will be removed. - */ - mount?: InputMaybe>; /** * Plain-text mount name (case insensitive) * * Added in Saleor 3.22. */ mountName?: InputMaybe>; - /** - * DEPRECATED: Use `targetName` instead. - * - * DEPRECATED: this field will be removed. - */ - target?: InputMaybe; /** * Plain-text target name (case insensitive) * @@ -1127,92 +1099,6 @@ export type AppExtensionFilterInput = { targetName?: InputMaybe; }; -/** All places where app extension can be mounted. */ -export type AppExtensionMountEnum = - | 'CATEGORY_DETAILS_MORE_ACTIONS' - | 'CATEGORY_OVERVIEW_CREATE' - | 'CATEGORY_OVERVIEW_MORE_ACTIONS' - | 'COLLECTION_DETAILS_MORE_ACTIONS' - | 'COLLECTION_DETAILS_WIDGETS' - | 'COLLECTION_OVERVIEW_CREATE' - | 'COLLECTION_OVERVIEW_MORE_ACTIONS' - | 'CUSTOMER_DETAILS_MORE_ACTIONS' - | 'CUSTOMER_DETAILS_WIDGETS' - | 'CUSTOMER_OVERVIEW_CREATE' - | 'CUSTOMER_OVERVIEW_MORE_ACTIONS' - | 'DISCOUNT_DETAILS_MORE_ACTIONS' - | 'DISCOUNT_OVERVIEW_CREATE' - | 'DISCOUNT_OVERVIEW_MORE_ACTIONS' - | 'DRAFT_ORDER_DETAILS_MORE_ACTIONS' - | 'DRAFT_ORDER_DETAILS_WIDGETS' - | 'DRAFT_ORDER_OVERVIEW_CREATE' - | 'DRAFT_ORDER_OVERVIEW_MORE_ACTIONS' - | 'GIFT_CARD_DETAILS_MORE_ACTIONS' - | 'GIFT_CARD_DETAILS_WIDGETS' - | 'GIFT_CARD_OVERVIEW_CREATE' - | 'GIFT_CARD_OVERVIEW_MORE_ACTIONS' - | 'MENU_DETAILS_MORE_ACTIONS' - | 'MENU_OVERVIEW_CREATE' - | 'MENU_OVERVIEW_MORE_ACTIONS' - | 'NAVIGATION_CATALOG' - | 'NAVIGATION_CUSTOMERS' - | 'NAVIGATION_DISCOUNTS' - | 'NAVIGATION_ORDERS' - | 'NAVIGATION_PAGES' - | 'NAVIGATION_TRANSLATIONS' - | 'ORDER_DETAILS_MORE_ACTIONS' - | 'ORDER_DETAILS_WIDGETS' - | 'ORDER_OVERVIEW_CREATE' - | 'ORDER_OVERVIEW_MORE_ACTIONS' - | 'PAGE_DETAILS_MORE_ACTIONS' - | 'PAGE_OVERVIEW_CREATE' - | 'PAGE_OVERVIEW_MORE_ACTIONS' - | 'PAGE_TYPE_DETAILS_MORE_ACTIONS' - | 'PAGE_TYPE_OVERVIEW_CREATE' - | 'PAGE_TYPE_OVERVIEW_MORE_ACTIONS' - | 'PRODUCT_DETAILS_MORE_ACTIONS' - | 'PRODUCT_DETAILS_WIDGETS' - | 'PRODUCT_OVERVIEW_CREATE' - | 'PRODUCT_OVERVIEW_MORE_ACTIONS' - | 'TRANSLATIONS_MORE_ACTIONS' - | 'VOUCHER_DETAILS_MORE_ACTIONS' - | 'VOUCHER_DETAILS_WIDGETS' - | 'VOUCHER_OVERVIEW_CREATE' - | 'VOUCHER_OVERVIEW_MORE_ACTIONS'; - -/** Represents the options for an app extension. */ -export type AppExtensionOptionsNewTab = { - /** - * Options controlling behavior of the NEW_TAB extension target - * @deprecated Use `settings` field directly. - */ - newTabTarget: Maybe; -}; - -/** Represents the options for an app extension. */ -export type AppExtensionOptionsWidget = { - /** - * Options for displaying a Widget - * @deprecated Use `settings` field directly. - */ - widgetTarget: Maybe; -}; - -export type AppExtensionPossibleOptions = AppExtensionOptionsNewTab | AppExtensionOptionsWidget; - -/** - * All available ways of opening an app extension. - * - * POPUP - app's extension will be mounted as a popup window - * APP_PAGE - redirect to app's page - * - */ -export type AppExtensionTargetEnum = - | 'APP_PAGE' - | 'NEW_TAB' - | 'POPUP' - | 'WIDGET'; - /** * Fetch and validate manifest. * @@ -1257,9 +1143,9 @@ export type AppInstallInput = { /** Determine if app will be set active or not. */ activateAfterInstallation?: InputMaybe; /** Name of the app to install. */ - appName?: InputMaybe; + appName: Scalars['String']['input']; /** URL to app's manifest in JSON format. */ - manifestUrl?: InputMaybe; + manifestUrl: Scalars['String']['input']; /** List of permission code names to assign to this app. */ permissions?: InputMaybe>; }; @@ -1321,12 +1207,7 @@ export type AppManifestExtension = { /** Label of the extension to show in the dashboard. */ label: Scalars['String']['output']; /** - * Place where given extension will be mounted. - * @deprecated Use `mountName` instead. - */ - mount: AppExtensionMountEnum; - /** - * Name of the extension mount point in the dashboard. Replaces `mount` + * Name of the extension mount point in the dashboard. Value returned in UPPERCASE. * * Added in Saleor 3.22. */ @@ -1334,18 +1215,13 @@ export type AppManifestExtension = { /** List of the app extension's permissions. */ permissions: Array; /** - * JSON object with settings for this extension. + * App extension settings. * * Added in Saleor 3.22. */ settings: Scalars['JSON']['output']; /** - * Type of way how app extension will be opened. - * @deprecated Use `targetName` instead. - */ - target: AppExtensionTargetEnum; - /** - * Name of the extension target in the dashboard. Replaces `target` + * Name of the extension target in the dashboard. Value returned in UPPERCASE. * * Added in Saleor 3.22. */ @@ -2188,7 +2064,7 @@ export type Attribute = Node & ObjectWithMetadata & { /** Public metadata. Use `keys` to control which fields you want to include. The default is to include everything. */ metafields: Maybe; /** Name of an attribute displayed in the interface. */ - name: Maybe; + name: Scalars['String']['output']; /** List of private metadata items. Requires staff permissions to access. */ privateMetadata: Array; /** @@ -2210,7 +2086,7 @@ export type Attribute = Node & ObjectWithMetadata & { */ referenceTypes: Maybe>; /** Internal representation of an attribute name. */ - slug: Maybe; + slug: Scalars['String']['output']; /** * The position of the attribute in the storefront navigation (0 by default). Requires one of the following permissions: MANAGE_PAGES, MANAGE_PAGE_TYPES_AND_ATTRIBUTES, MANAGE_PRODUCTS, MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES. * @deprecated No longer supported @@ -2219,7 +2095,7 @@ export type Attribute = Node & ObjectWithMetadata & { /** Returns translated attribute fields for the given language code. */ translation: Maybe; /** The attribute type. */ - type: Maybe; + type: AttributeTypeEnum; /** The unit of attribute values. */ unit: Maybe; /** Whether the attribute requires values to be passed or not. Requires one of the following permissions: MANAGE_PAGES, MANAGE_PAGE_TYPES_AND_ATTRIBUTES, MANAGE_PRODUCTS, MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES. */ @@ -4354,12 +4230,19 @@ export type Checkout = Node & ObjectWithMetadata & { * Added in Saleor 3.21. */ customerNote: Scalars['String']['output']; + /** + * The delivery method selected for this checkout. + * + * Added in Saleor 3.23. + */ + delivery: Maybe; /** * The delivery method selected for this checkout. * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Optionally triggered when cached external shipping methods are invalid. * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. + * @deprecated Use `delivery` instead. */ deliveryMethod: Maybe; /** The total discount applied to the checkout. Note: Only discount created via voucher are included in this field. */ @@ -4419,7 +4302,7 @@ export type Checkout = Node & ObjectWithMetadata & { * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Optionally triggered when cached external shipping methods are invalid. * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. - * @deprecated Use `deliveryMethod` instead. + * @deprecated Use `delivery` instead. */ shippingMethod: Maybe; /** @@ -4825,6 +4708,7 @@ export type CheckoutCustomerNoteUpdate = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ export type CheckoutDeliveryMethodUpdate = { @@ -5223,7 +5107,25 @@ export type CheckoutPaymentCreate = { }; /** Represents an problem in the checkout. */ -export type CheckoutProblem = CheckoutLineProblemInsufficientStock | CheckoutLineProblemVariantNotAvailable; +export type CheckoutProblem = CheckoutLineProblemInsufficientStock | CheckoutLineProblemVariantNotAvailable | CheckoutProblemDeliveryMethodInvalid | CheckoutProblemDeliveryMethodStale; + +/** + * Indicates that the selected delivery method is invalid. + * + * Added in Saleor 3.23. + */ +export type CheckoutProblemDeliveryMethodInvalid = { + delivery: Delivery; +}; + +/** + * Indicates that the delivery methods are stale. + * + * Added in Saleor 3.23. + */ +export type CheckoutProblemDeliveryMethodStale = { + delivery: Delivery; +}; /** * Remove a gift card or a voucher from a checkout. @@ -5241,6 +5143,12 @@ export type CheckoutRemovePromoCode = { /** Represents the channel-specific checkout settings. */ export type CheckoutSettings = { + /** + * Default to `true`. Determines whether gift cards can be attached to a Checkout via `addPromoCode` mutation. Usage of this mutation with gift cards is deprecated. + * + * Added in Saleor 3.23. + */ + allowLegacyGiftCardUse: Scalars['Boolean']['output']; /** * The date time defines the earliest checkout creation date on which fully paid checkouts can begin to be automatically completed. * @@ -5268,6 +5176,12 @@ export type CheckoutSettings = { }; export type CheckoutSettingsInput = { + /** + * Default to `true`. Determines whether gift cards can be attached to a Checkout via `addPromoCode` mutation. Usage of this mutation with gift cards is deprecated. + * + * Added in Saleor 3.23. + */ + allowLegacyGiftCardUse?: InputMaybe; /** * Settings for automatic completion of fully paid checkouts. * @@ -5309,6 +5223,7 @@ export type CheckoutShippingAddressUpdate = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout shipping method with the external one. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ export type CheckoutShippingMethodUpdate = { @@ -5325,7 +5240,9 @@ export type CheckoutSortField = /** Sort checkouts by customer. */ | 'CUSTOMER' /** Sort checkouts by payment. */ - | 'PAYMENT'; + | 'PAYMENT' + /** Sort checkouts by rank. Note: This option is available only with the `search` filter. */ + | 'RANK'; export type CheckoutSortingInput = { /** Specifies the direction in which to sort checkouts. */ @@ -6749,8 +6666,6 @@ export type CustomerEvent = Node & { message: Maybe; /** The concerned order. */ order: Maybe; - /** The concerned order line. */ - orderLine: Maybe; /** Customer event type. */ type: Maybe; /** User who performed the action. */ @@ -7016,208 +6931,50 @@ export type DeletePrivateMetadata = { metadataErrors: Array; }; -/** Represents a delivery method chosen for the checkout. `Warehouse` type is used when checkout is marked as "click and collect" and `ShippingMethod` otherwise. */ -export type DeliveryMethod = ShippingMethod | Warehouse; - -/** Represents digital content associated with a product variant. */ -export type DigitalContent = Node & ObjectWithMetadata & { - /** Indicator for automatic fulfillment of digital content. */ - automaticFulfillment: Scalars['Boolean']['output']; - /** File associated with digital content. */ - contentFile: Scalars['String']['output']; - /** The ID of the digital content. */ - id: Scalars['ID']['output']; - /** Maximum number of allowed downloads for the digital content. */ - maxDownloads: Maybe; - /** List of public metadata items. Can be accessed without permissions. */ - metadata: Array; - /** - * A single key from public metadata. - * - * Tip: Use GraphQL aliases to fetch multiple keys. - */ - metafield: Maybe; - /** Public metadata. Use `keys` to control which fields you want to include. The default is to include everything. */ - metafields: Maybe; - /** List of private metadata items. Requires staff permissions to access. */ - privateMetadata: Array; - /** - * A single key from private metadata. Requires staff permissions to access. - * - * Tip: Use GraphQL aliases to fetch multiple keys. - */ - privateMetafield: Maybe; - /** Private metadata. Requires staff permissions to access. Use `keys` to control which fields you want to include. The default is to include everything. */ - privateMetafields: Maybe; - /** Product variant assigned to digital content. */ - productVariant: ProductVariant; - /** Number of days the URL for the digital content remains valid. */ - urlValidDays: Maybe; - /** List of URLs for the digital variant. */ - urls: Maybe>; - /** Default settings indicator for digital content. */ - useDefaultSettings: Scalars['Boolean']['output']; -}; - - -/** Represents digital content associated with a product variant. */ -export type DigitalContentMetafieldArgs = { - key: Scalars['String']['input']; -}; - - -/** Represents digital content associated with a product variant. */ -export type DigitalContentMetafieldsArgs = { - keys?: InputMaybe>; -}; - - -/** Represents digital content associated with a product variant. */ -export type DigitalContentPrivateMetafieldArgs = { - key: Scalars['String']['input']; -}; - - -/** Represents digital content associated with a product variant. */ -export type DigitalContentPrivateMetafieldsArgs = { - keys?: InputMaybe>; -}; - -/** A connection to a list of digital content items. */ -export type DigitalContentCountableConnection = { - edges: Array; - /** Pagination data for this connection. */ - pageInfo: PageInfo; - /** A total count of items in the collection. */ - totalCount: Maybe; -}; - -export type DigitalContentCountableEdge = { - /** A cursor for use in pagination. */ - cursor: Scalars['String']['output']; - /** The item at the end of the edge. */ - node: DigitalContent; -}; - /** - * Create new digital content. This mutation must be sent as a `multipart` request. More detailed specs of the upload format can be found here: https://github.com/jaydenseric/graphql-multipart-request-spec + * Represents a delivery option for the checkout. * - * Requires one of the following permissions: MANAGE_PRODUCTS. - */ -export type DigitalContentCreate = { - content: Maybe; - errors: Array; - /** @deprecated Use `errors` field instead. */ - productErrors: Array; - variant: Maybe; -}; - -/** - * Remove digital content assigned to given variant. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. + * Added in Saleor 3.23. */ -export type DigitalContentDelete = { - errors: Array; - /** @deprecated Use `errors` field instead. */ - productErrors: Array; - variant: Maybe; +export type Delivery = { + /** The ID of the delivery. */ + id: Scalars['ID']['output']; + /** Shipping method represented by the delivery. */ + shippingMethod: Maybe; }; -export type DigitalContentInput = { - /** Overwrite default automatic_fulfillment setting for variant. */ - automaticFulfillment?: InputMaybe; - /** Determines how many times a download link can be accessed by a customer. */ - maxDownloads?: InputMaybe; - /** - * Fields required to update the digital content metadata. Can be read by any API client authorized to read the object it's attached to. - * - * Warning: never store sensitive information, including financial data such as credit card details. - */ - metadata?: InputMaybe>; - /** - * Fields required to update the digital content private metadata. Requires permissions to modify and to read the metadata of the object it's attached to. - * - * Warning: never store sensitive information, including financial data such as credit card details. - */ - privateMetadata?: InputMaybe>; - /** Determines for how many days a download link is active since it was generated. */ - urlValidDays?: InputMaybe; - /** Use default digital content settings for this product. */ - useDefaultSettings: Scalars['Boolean']['input']; -}; +/** Represents a delivery method chosen for the checkout. `Warehouse` type is used when checkout is marked as "click and collect" and `ShippingMethod` otherwise. */ +export type DeliveryMethod = ShippingMethod | Warehouse; /** - * Updates digital content. + * Calculates available delivery options for a checkout. * - * Requires one of the following permissions: MANAGE_PRODUCTS. - */ -export type DigitalContentUpdate = { - content: Maybe; - errors: Array; - /** @deprecated Use `errors` field instead. */ - productErrors: Array; - variant: Maybe; -}; - -export type DigitalContentUploadInput = { - /** Overwrite default automatic_fulfillment setting for variant. */ - automaticFulfillment?: InputMaybe; - /** Represents an file in a multipart request. */ - contentFile: Scalars['Upload']['input']; - /** Determines how many times a download link can be accessed by a customer. */ - maxDownloads?: InputMaybe; - /** - * Fields required to update the digital content metadata. Can be read by any API client authorized to read the object it's attached to. - * - * Warning: never store sensitive information, including financial data such as credit card details. - */ - metadata?: InputMaybe>; - /** - * Fields required to update the digital content private metadata. Requires permissions to modify and to read the metadata of the object it's attached to. - * - * Warning: never store sensitive information, including financial data such as credit card details. - */ - privateMetadata?: InputMaybe>; - /** Determines for how many days a download link is active since it was generated. */ - urlValidDays?: InputMaybe; - /** Use default digital content settings for this product. */ - useDefaultSettings: Scalars['Boolean']['input']; -}; - -/** Represents a URL for digital content. */ -export type DigitalContentUrl = Node & { - /** Digital content associated with the URL. */ - content: DigitalContent; - /** Date and time when the digital content URL was created. */ - created: Scalars['DateTime']['output']; - /** Number of times digital content has been downloaded. */ - downloadNum: Scalars['Int']['output']; - /** The ID of the digital content URL. */ - id: Scalars['ID']['output']; - /** UUID of digital content. */ - token: Scalars['UUID']['output']; - /** URL for digital content. */ - url: Maybe; -}; - -/** - * Generate new URL to digital content. + * Added in Saleor 3.23. * - * Requires one of the following permissions: MANAGE_PRODUCTS. + * Triggers the following webhook events: + * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered to fetch external shipping methods. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Triggered to filter shipping methods. */ -export type DigitalContentUrlCreate = { - digitalContentUrl: Maybe; - errors: Array; - /** @deprecated Use `errors` field instead. */ - productErrors: Array; +export type DeliveryOptionsCalculate = { + /** List of the available deliveries. */ + deliveries: Array; + errors: Array; }; -export type DigitalContentUrlCreateInput = { - /** Digital content ID which URL will belong to. */ - content: Scalars['ID']['input']; +export type DeliveryOptionsCalculateError = { + /** The error code. */ + code: DeliveryOptionsCalculateErrorCode; + /** Name of a field that caused the error. A value of `null` indicates that the error isn't associated with a particular field. */ + field: Maybe; + /** The error message. */ + message: Maybe; }; +export type DeliveryOptionsCalculateErrorCode = + | 'GRAPHQL_ERROR' + | 'INVALID' + | 'NOT_FOUND'; + export type DiscountError = { /** List of channels IDs which causes the error. */ channels: Maybe>; @@ -7387,7 +7144,11 @@ export type DraftOrderCreateInput = { user?: InputMaybe; /** Email address of the customer. */ userEmail?: InputMaybe; - /** ID of the voucher associated with the order. */ + /** + * ID of the voucher associated with the order. + * + * DEPRECATED: this field will be removed. Use `voucherCode` instead. + */ voucher?: InputMaybe; /** * A code of the voucher associated with the order. @@ -7496,7 +7257,11 @@ export type DraftOrderInput = { user?: InputMaybe; /** Email address of the customer. */ userEmail?: InputMaybe; - /** ID of the voucher associated with the order. */ + /** + * ID of the voucher associated with the order. + * + * DEPRECATED: this field will be removed. Use `voucherCode` instead. + */ voucher?: InputMaybe; /** * A code of the voucher associated with the order. @@ -7910,8 +7675,6 @@ export type ExportScope = * * Added in Saleor 3.18. * - * Note: this API is currently in Feature Preview and can be subject to changes at later point. - * * Requires one of the following permissions: MANAGE_DISCOUNTS. * * Triggers the following webhook events: @@ -8392,7 +8155,7 @@ export type GiftCard = Node & ObjectWithMetadata & { */ endDate: Maybe; /** - * List of events associated with the gift card. Requires MANAGE_GIFT_CARD permission to access all events. Users with MANAGE_ORDERS permission can access only USED_IN_ORDER events. + * List of events associated with the gift card. Requires MANAGE_GIFT_CARD permission to access all events. Users with MANAGE_ORDERS permission can access only USED_IN_ORDER and REFUNDED_IN_ORDER events. * * Requires one of the following permissions: MANAGE_GIFT_CARD, MANAGE_ORDERS. */ @@ -8814,6 +8577,7 @@ export type GiftCardEventsEnum = | 'EXPIRY_DATE_UPDATED' | 'ISSUED' | 'NOTE_ADDED' + | 'REFUNDED_IN_ORDER' | 'RESENT' | 'SENT_TO_CUSTOMER' | 'TAGS_UPDATED' @@ -8862,6 +8626,55 @@ export type GiftCardMetadataUpdated = Event & { version: Maybe; }; +/** + * Represents a gift card payment method used for a transaction. + * + * Added in Saleor 3.23. + */ +export type GiftCardPaymentMethodDetails = PaymentMethodDetails & { + /** + * Brand of the gift card. + * + * Added in Saleor 3.23. + */ + brand: Maybe; + /** + * Indicates whether the gift card is a built-in Saleor gift card. + * + * Added in Saleor 3.23. + */ + isSaleorGiftcard: Scalars['Boolean']['output']; + /** + * Last characters of the gift card code. Max 4 characters. + * + * Added in Saleor 3.23. + */ + lastChars: Maybe; + /** Name of the gift card. */ + name: Scalars['String']['output']; +}; + +export type GiftCardPaymentMethodDetailsInput = { + /** + * Brand of the gift card used for the transaction. Max length is 40 characters. + * + * Added in Saleor 3.23. + */ + brand?: InputMaybe; + /** + * Last characters of the gift card used for the transaction. Max length is 4 characters. + * + * Added in Saleor 3.23. + */ + lastChars?: InputMaybe; + /** + * Name of the payment method used for the transaction. Max length is 256 characters. + * + * Added in Saleor 3.23. + */ + name: Scalars['String']['input']; +}; + /** * Resend a gift card. * @@ -8954,6 +8767,8 @@ export type GiftCardSortField = | 'CURRENT_BALANCE' /** Sort gift cards by product. */ | 'PRODUCT' + /** Sort gift cards by rank. Note: This option is available only with the `search` filter. */ + | 'RANK' /** Sort gift cards by used by. */ | 'USED_BY'; @@ -9118,10 +8933,6 @@ export type GroupCountableEdge = { node: Group; }; -export type HttpMethod = - | 'GET' - | 'POST'; - /** Thumbnail formats for icon images. */ export type IconThumbnailFormatEnum = | 'ORIGINAL' @@ -12345,6 +12156,7 @@ export type Mutation = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ checkoutDeliveryMethodUpdate: Maybe; @@ -12412,6 +12224,7 @@ export type Mutation = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout shipping method with the external one. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. * @deprecated Use `checkoutDeliveryMethodUpdate` instead. */ @@ -12555,33 +12368,15 @@ export type Mutation = { */ deleteWarehouse: Maybe; /** - * Create new digital content. This mutation must be sent as a `multipart` request. More detailed specs of the upload format can be found here: https://github.com/jaydenseric/graphql-multipart-request-spec + * Calculates available delivery options for a checkout. * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContentCreate: Maybe; - /** - * Remove digital content assigned to given variant. + * Added in Saleor 3.23. * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContentDelete: Maybe; - /** - * Updates digital content. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContentUpdate: Maybe; - /** - * Generate new URL to digital content. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + * Triggers the following webhook events: + * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered to fetch external shipping methods. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Triggered to filter shipping methods. */ - digitalContentUrlCreate: Maybe; + deliveryOptionsCalculate: Maybe; /** * Deletes draft orders. * @@ -12633,6 +12428,7 @@ export type Mutation = { * Triggers the following webhook events: * - NOTIFY_USER (async): A notification for the exported file. * - GIFT_CARD_EXPORT_COMPLETED (async): A notification for the exported file. + * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportGiftCards: Maybe; /** @@ -12643,6 +12439,7 @@ export type Mutation = { * Triggers the following webhook events: * - NOTIFY_USER (async): A notification for the exported file. * - PRODUCT_EXPORT_COMPLETED (async): A notification for the exported file. + * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportProducts: Maybe; /** @@ -12650,12 +12447,11 @@ export type Mutation = { * * Added in Saleor 3.18. * - * Note: this API is currently in Feature Preview and can be subject to changes at later point. - * * Requires one of the following permissions: MANAGE_DISCOUNTS. * * Triggers the following webhook events: * - VOUCHER_CODE_EXPORT_COMPLETED (async): A notification for the exported file. + * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportVoucherCodes: Maybe; /** Prepare external authentication URL for user by custom plugin. */ @@ -14757,25 +14553,8 @@ export type MutationDeleteWarehouseArgs = { }; -export type MutationDigitalContentCreateArgs = { - input: DigitalContentUploadInput; - variantId: Scalars['ID']['input']; -}; - - -export type MutationDigitalContentDeleteArgs = { - variantId: Scalars['ID']['input']; -}; - - -export type MutationDigitalContentUpdateArgs = { - input: DigitalContentInput; - variantId: Scalars['ID']['input']; -}; - - -export type MutationDigitalContentUrlCreateArgs = { - input: DigitalContentUrlCreateInput; +export type MutationDeliveryOptionsCalculateArgs = { + id: Scalars['ID']['input']; }; @@ -16132,15 +15911,6 @@ export type NavigationType = /** Secondary storefront navigation. */ | 'SECONDARY'; -/** Represents the NEW_TAB target options for an app extension. */ -export type NewTabTargetOptions = { - /** - * HTTP method for New Tab target (GET or POST) - * @deprecated Use `settings` field directly. - */ - method: HttpMethod; -}; - /** An object with an ID */ export type Node = { /** The ID of the object. */ @@ -17783,7 +17553,6 @@ export type OrderLine = Node & ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_PRODUCTS, MANAGE_ORDERS. */ allocations: Maybe>; - digitalContentUrl: Maybe; /** * List of applied discounts * @@ -18948,6 +18717,8 @@ export type PageSortField = | 'PUBLICATION_DATE' /** Sort pages by publication date. */ | 'PUBLISHED_AT' + /** Sort pages by rank. Note: This option is available only with the `search` filter. */ + | 'RANK' /** Sort pages by slug. */ | 'SLUG' /** Sort pages by title. */ @@ -19287,7 +19058,7 @@ export type PageTypeUpdateInput = { addAttributes?: InputMaybe>; /** Name of the page type. */ name?: InputMaybe; - /** List of attribute IDs to be assigned to the page type. */ + /** List of attribute IDs to be unassigned from the page type. */ removeAttributes?: InputMaybe>; /** Page type slug. */ slug?: InputMaybe; @@ -19362,6 +19133,21 @@ export type PasswordChange = { user: Maybe; }; +/** + * Controls whether password-based authentication is allowed. + * + * ENABLED - any user can log in with a password. This is the default behavior. + * CUSTOMERS_ONLY - only customer users can log in with a password. + * If a staff user logs in with a password, they will be treated as a customer + * — the issued token will not contain any staff permissions. + * DISABLED - no user can log in with a password. + * + */ +export type PasswordLoginModeEnum = + | 'CUSTOMERS_ONLY' + | 'DISABLED' + | 'ENABLED'; + /** Represents a payment of a given type. */ export type Payment = Node & ObjectWithMetadata & { /** @@ -19418,11 +19204,6 @@ export type Payment = Node & ObjectWithMetadata & { modified: Scalars['DateTime']['output']; /** Order associated with a payment. */ order: Maybe; - /** - * Informs whether this is a partial payment. - * @deprecated This field is reserved for the Adyen Gateway plugin. For other gateways, its value is always `false`. This field will be removed in 3.23 along with the plugin. - */ - partial: Scalars['Boolean']['output']; /** Type of method used for payment. */ paymentMethodType: Scalars['String']['output']; /** List of private metadata items. Requires staff permissions to access. */ @@ -19830,13 +19611,19 @@ export type PaymentMethodDetailsFilterInput = { }; /** - * Details of the payment method used for the transaction. One of `card` or `other` is required. + * Details of the payment method used for the transaction. One of `card`, `other`, or `giftCard` is required. * * Added in Saleor 3.22. */ export type PaymentMethodDetailsInput = { /** Details of the card payment method used for the transaction. */ card?: InputMaybe; + /** + * Details of the gift card payment method used for the transaction. + * + * Added in Saleor 3.23. + */ + giftCard?: InputMaybe; /** Details of the non-card payment method used for this transaction. */ other?: InputMaybe; }; @@ -19982,11 +19769,13 @@ export type PaymentMethodTokenizationResult = * The following types are possible: * CARD - represents a card payment method. * OTHER - represents any payment method that is not a card payment. + * GIFT_CARD - represents a gift card payment method. * * */ export type PaymentMethodTypeEnum = | 'CARD' + | 'GIFT_CARD' | 'OTHER'; export type PaymentMethodTypeEnumFilterInput = { @@ -21270,8 +21059,7 @@ export type ProductErrorCode = | 'REQUIRED' | 'UNIQUE' | 'UNSUPPORTED_MEDIA_PROVIDER' - | 'UNSUPPORTED_MIME_TYPE' - | 'VARIANT_NO_DIGITAL_CONTENT'; + | 'UNSUPPORTED_MIME_TYPE'; /** Event sent when product export is completed. */ export type ProductExportCompleted = Event & { @@ -21866,11 +21654,17 @@ export type ProductType = Node & ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_PRODUCTS. */ availableAttributes: Maybe; - /** Whether the product type has variants. */ + /** + * Whether the product type has variants. + * @deprecated This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. + */ hasVariants: Scalars['Boolean']['output']; /** The ID of the product type. */ id: Scalars['ID']['output']; - /** Whether the product type is digital. */ + /** + * Whether the product type is digital - doesn't have any effect, it's present for backward-compatibility. + * @deprecated Will be removed in v3.24.0, use metadata or attributes instead. + */ isDigital: Scalars['Boolean']['output']; /** Whether shipping is required for this product type. */ isShippingRequired: Scalars['Boolean']['output']; @@ -22046,6 +21840,11 @@ export type ProductTypeEnum = | 'SHIPPABLE'; export type ProductTypeFilterInput = { + /** + * + * + * DEPRECATED: this field will be removed. The field has no effect on the API behavior. This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. + */ configurable?: InputMaybe; ids?: InputMaybe>; kind?: InputMaybe; @@ -22056,9 +21855,13 @@ export type ProductTypeFilterInput = { }; export type ProductTypeInput = { - /** Determines if product of this type has multiple variants. This option mainly simplifies product management in the dashboard. There is always at least one variant created under the hood. */ + /** + * Determines if product of this type has multiple variants. This option mainly simplifies product management in the dashboard. There is always at least one variant created under the hood. + * + * DEPRECATED: this field will be removed. The field has no effect on the API behavior. This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. + */ hasVariants?: InputMaybe; - /** Determines if products are digital. */ + /** Determines if products are digital - doesn't have any effect, it's present for backward-compatibility. */ isDigital?: InputMaybe; /** Determines if shipping is required for products of this variant. */ isShippingRequired?: InputMaybe; @@ -22191,12 +21994,6 @@ export type ProductVariant = Node & ObjectWithAttributes & ObjectWithMetadata & channelListings: Maybe>; /** The date and time when the product variant was created. */ created: Scalars['DateTime']['output']; - /** - * Digital content for the product variant. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - */ - digitalContent: Maybe; /** External ID of this product. */ externalReference: Maybe; /** The ID of the product variant. */ @@ -22596,7 +22393,9 @@ export type ProductVariantChannelListing = Node & { /** The price of the variant. */ price: Maybe; /** - * Prior price of the variant used for discount calculations. + * Previous price of the variant in channel. Useful for providing promotion information required by customer protection laws such as EU Omnibus directive. + * + * Warning: This field is not updated automatically. Use Channel Listings mutation to update it manually. * * Added in Saleor 3.21. */ @@ -24221,20 +24020,6 @@ export type Query = { * Requires one of the following permissions: MANAGE_ORDERS, MANAGE_USERS. */ customers: Maybe; - /** - * Look up digital content by ID. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContent: Maybe; - /** - * List of digital content. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContents: Maybe; /** * List of draft orders. The query will not initiate any external requests, including filtering available shipping methods, or performing external tax calculations. * @@ -24562,7 +24347,7 @@ export type Query = { export type Query_EntitiesArgs = { - representations?: InputMaybe>>; + representations: Array; }; @@ -24710,19 +24495,6 @@ export type QueryCustomersArgs = { }; -export type QueryDigitalContentArgs = { - id: Scalars['ID']['input']; -}; - - -export type QueryDigitalContentsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - export type QueryDraftOrdersArgs = { after?: InputMaybe; before?: InputMaybe; @@ -25116,6 +24888,7 @@ export type QueryTransactionsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortBy?: InputMaybe; where?: InputMaybe; }; @@ -25260,7 +25033,7 @@ export type RefundSettingsErrorCode = export type RefundSettingsUpdate = { errors: Array; /** Refund settings. */ - refundSettings: RefundSettings; + refundSettings: Maybe; /** @deprecated Use `errors` field instead. */ refundSettingsErrors: Array; }; @@ -26011,7 +25784,10 @@ export type ShippingMethod = Node & ObjectWithMetadata & { id: Scalars['ID']['output']; /** Maximum delivery days for this shipping method. */ maximumDeliveryDays: Maybe; - /** Maximum order price for this shipping method. */ + /** + * Maximum order price for this shipping method. + * @deprecated No longer supported + */ maximumOrderPrice: Maybe; /** * Maximum order weight for this shipping method. @@ -26032,7 +25808,10 @@ export type ShippingMethod = Node & ObjectWithMetadata & { metafields: Maybe; /** Minimum delivery days for this shipping method. */ minimumDeliveryDays: Maybe; - /** Minimal order price for this shipping method. */ + /** + * Minimal order price for this shipping method. + * @deprecated No longer supported + */ minimumOrderPrice: Maybe; /** * Minimum order weight for this shipping method. @@ -26803,12 +26582,6 @@ export type Shop = ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_SETTINGS. */ allowLoginWithoutConfirmation: Maybe; - /** - * Enable automatic fulfillment for all digital products. - * - * Requires one of the following permissions: MANAGE_SETTINGS. - */ - automaticFulfillmentDigitalProducts: Maybe; /** List of available external authentications. */ availableExternalAuthentications: Array; /** List of available payment gateways. */ @@ -26842,18 +26615,6 @@ export type Shop = ObjectWithMetadata & { customerSetPasswordUrl: Maybe; /** Shop's default country. */ defaultCountry: Maybe; - /** - * Default number of max downloads per digital content URL. - * - * Requires one of the following permissions: MANAGE_SETTINGS. - */ - defaultDigitalMaxDownloads: Maybe; - /** - * Default number of days which digital content URL will be valid. - * - * Requires one of the following permissions: MANAGE_SETTINGS. - */ - defaultDigitalUrlValidDays: Maybe; /** * Default shop's email sender's address. * @@ -26923,6 +26684,12 @@ export type Shop = ObjectWithMetadata & { metafields: Maybe; /** Shop's name. */ name: Scalars['String']['output']; + /** + * Controls whether password-based authentication is allowed. + * + * Added in Saleor 3.23. + */ + passwordLoginMode: PasswordLoginModeEnum; /** List of available permissions. */ permissions: Array; /** List of possible phone prefixes. */ @@ -26969,6 +26736,12 @@ export type Shop = ObjectWithMetadata & { trackInventoryByDefault: Maybe; /** Returns translated shop fields for the given language code. */ translation: Maybe; + /** + * When enabled, stock availability is filtered by shipping zones and the destination address (legacy behavior). When disabled, stock availability is determined only by the direct warehouse-channel link, ignoring shipping zones. + * + * Added in Saleor 3.23. + */ + useLegacyShippingZoneStockAvailability: Scalars['Boolean']['output']; /** * Use legacy update webhook emission. When enabled, update webhooks (e.g. `customerUpdated`,`productVariantUpdated`) are sent even when only metadata changes. When disabled, update webhooks are not sent for metadata-only changes; only metadata-specific webhooks (e.g., `customerMetadataUpdated`, `productVariantMetadataUpdated`) are sent. * @@ -27076,6 +26849,7 @@ export type ShopErrorCode = | 'GRAPHQL_ERROR' | 'INVALID' | 'NOT_FOUND' + | 'PASSWORD_AUTH_RESTRICTION' | 'REQUIRED' | 'UNIQUE'; @@ -27109,8 +26883,6 @@ export type ShopMetadataUpdated = Event & { export type ShopSettingsInput = { /** Enable possibility to login without account confirmation. */ allowLoginWithoutConfirmation?: InputMaybe; - /** Enable automatic fulfillment for all digital products. */ - automaticFulfillmentDigitalProducts?: InputMaybe; /** * Charge taxes on shipping. * @@ -27119,10 +26891,6 @@ export type ShopSettingsInput = { chargeTaxesOnShipping?: InputMaybe; /** URL of a view where customers can set their password. */ customerSetPasswordUrl?: InputMaybe; - /** Default number of max downloads per digital content URL. */ - defaultDigitalMaxDownloads?: InputMaybe; - /** Default number of days which digital content URL will be valid. */ - defaultDigitalUrlValidDays?: InputMaybe; /** Default email sender's address. */ defaultMailSenderAddress?: InputMaybe; /** Default email sender's name. */ @@ -27159,6 +26927,12 @@ export type ShopSettingsInput = { * Warning: never store sensitive information, including financial data such as credit card details. */ metadata?: InputMaybe>; + /** + * Controls whether password-based authentication is allowed. + * + * Added in Saleor 3.23. + */ + passwordLoginMode?: InputMaybe; /** * When enabled, address fields that are not valid for a given country (according to Google's i18n address data) will be preserved instead of being removed during validation. Validation errors are still returned. * @@ -27177,6 +26951,12 @@ export type ShopSettingsInput = { reserveStockDurationAuthenticatedUser?: InputMaybe; /** This field is used as a default value for `ProductVariant.trackInventory`. */ trackInventoryByDefault?: InputMaybe; + /** + * When enabled, stock availability is filtered by shipping zones and the destination address (legacy behavior). When disabled, stock availability is determined only by the direct warehouse-channel link, ignoring shipping zones. + * + * Added in Saleor 3.23. + */ + useLegacyShippingZoneStockAvailability?: InputMaybe; /** * Use legacy update webhook emission. When enabled, update webhooks (e.g. `customerUpdated`,`productVariantUpdated`) are sent even when only metadata changes. When disabled, update webhooks are not sent for metadata-only changes; only metadata-specific webhooks (e.g., `customerMetadataUpdated`, `productVariantMetadataUpdated`) are sent. * @@ -28859,6 +28639,26 @@ export type TransactionEvent = Node & { type: Maybe; }; +/** + * Filter input for transaction events data. + * + * Added in Saleor 3.23. + */ +export type TransactionEventFilterInput = { + /** + * Filter transaction events by created at date. + * + * Added in Saleor 3.23. + */ + createdAt?: InputMaybe; + /** + * Filter transaction events by type. + * + * Added in Saleor 3.23. + */ + type?: InputMaybe; +}; + export type TransactionEventInput = { /** The message related to the event. */ message?: InputMaybe; @@ -28951,6 +28751,13 @@ export type TransactionEventTypeEnum = | 'REFUND_REVERSE' | 'REFUND_SUCCESS'; +export type TransactionEventTypeEnumFilterInput = { + /** The value equal to. */ + eq?: InputMaybe; + /** The value included in. */ + oneOf?: InputMaybe>; +}; + /** Filter input for transactions. */ export type TransactionFilterInput = { /** Filter by metadata fields of transactions. */ @@ -29300,6 +29107,27 @@ export type TransactionRequestRefundForGrantedRefundErrorCode = | 'REFUND_ALREADY_PROCESSED' | 'REFUND_IS_PENDING'; +export type TransactionSortField = + /** + * Sort transactions by creation date. + * + * Added in Saleor 3.23. + */ + | 'CREATED_AT' + /** + * Sort transactions by modification date. + * + * Added in Saleor 3.23. + */ + | 'MODIFIED_AT'; + +export type TransactionSortingInput = { + /** Specifies the direction in which to sort transactions. */ + direction: OrderDirection; + /** Sort transactions by the selected field. */ + field: TransactionSortField; +}; + /** * Update transaction. * @@ -29373,7 +29201,25 @@ export type TransactionWhereInput = { OR?: InputMaybe>; /** Filter by app identifier. */ appIdentifier?: InputMaybe; + /** + * Filter transactions by created at date. + * + * Added in Saleor 3.23. + */ + createdAt?: InputMaybe; + /** + * Filter by transaction events. Each list item represents conditions that must be satisfied by a single event. The filter matches transactions that have related events meeting all specified groups of conditions. + * + * Added in Saleor 3.23. + */ + events?: InputMaybe>; ids?: InputMaybe>; + /** + * Filter transactions by modified at date. + * + * Added in Saleor 3.23. + */ + modifiedAt?: InputMaybe; /** Filter by PSP reference. */ pspReference?: InputMaybe; }; @@ -29847,7 +29693,9 @@ export type UserSortField = /** Sort users by last name. */ | 'LAST_NAME' /** Sort users by order count. */ - | 'ORDER_COUNT'; + | 'ORDER_COUNT' + /** Sort users by rank. Note: This option is available only with the `search` filter. */ + | 'RANK'; export type UserSortingInput = { /** Specifies the direction in which to sort users. */ @@ -32062,15 +31910,6 @@ export type WeightUnitsEnum = | 'OZ' | 'TONNE'; -/** Represents the WIDGET target options for an app extension. */ -export type WidgetTargetOptions = { - /** - * HTTP method for Widget target (GET or POST) - * @deprecated Use `settings` field directly. - */ - method: HttpMethod; -}; - /** _Entity union as defined by Federation spec. */ export type _Entity = Address | App | Category | Collection | Group | Order | PageType | Product | ProductMedia | ProductType | ProductVariant | User; @@ -33283,7 +33122,7 @@ export type ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttr export type ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute_choices_AttributeValueCountableConnection = { edges: Array }; -export type ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute = { id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute_choices_AttributeValueCountableConnection | null }; +export type ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute = { id: string, name: string, slug: string, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute_choices_AttributeValueCountableConnection | null }; export type ProductDetail_product_Product_assignedAttributes_AssignedFileAttribute_fileValue_File = { url: string, contentType: string | null }; @@ -33426,7 +33265,7 @@ export type ProductTypeDetail_productType_ProductType_productAttributes_Attribut export type ProductTypeDetail_productType_ProductType_productAttributes_Attribute_choices_AttributeValueCountableConnection = { edges: Array }; -export type ProductTypeDetail_productType_ProductType_productAttributes_Attribute = { id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductTypeDetail_productType_ProductType_productAttributes_Attribute_choices_AttributeValueCountableConnection | null }; +export type ProductTypeDetail_productType_ProductType_productAttributes_Attribute = { id: string, name: string, slug: string, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductTypeDetail_productType_ProductType_productAttributes_Attribute_choices_AttributeValueCountableConnection | null }; export type ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute_choices_AttributeValueCountableConnection_edges_AttributeValueCountableEdge_node_AttributeValue = { id: string, name: string | null, slug: string | null }; @@ -33434,7 +33273,7 @@ export type ProductTypeDetail_productType_ProductType_assignedVariantAttributes_ export type ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute_choices_AttributeValueCountableConnection = { edges: Array }; -export type ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute = { id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute_choices_AttributeValueCountableConnection | null }; +export type ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute = { id: string, name: string, slug: string, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute_choices_AttributeValueCountableConnection | null }; export type ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute = { variantSelection: boolean, attribute: ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute }; @@ -33456,7 +33295,7 @@ export type ProductTypesList_productTypes_ProductTypeCountableConnection_edges_P export type ProductTypesList_productTypes_ProductTypeCountableConnection_edges_ProductTypeCountableEdge_node_ProductType_productAttributes_Attribute_choices_AttributeValueCountableConnection = { edges: Array }; -export type ProductTypesList_productTypes_ProductTypeCountableConnection_edges_ProductTypeCountableEdge_node_ProductType_productAttributes_Attribute = { id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductTypesList_productTypes_ProductTypeCountableConnection_edges_ProductTypeCountableEdge_node_ProductType_productAttributes_Attribute_choices_AttributeValueCountableConnection | null }; +export type ProductTypesList_productTypes_ProductTypeCountableConnection_edges_ProductTypeCountableEdge_node_ProductType_productAttributes_Attribute = { id: string, name: string, slug: string, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductTypesList_productTypes_ProductTypeCountableConnection_edges_ProductTypeCountableEdge_node_ProductType_productAttributes_Attribute_choices_AttributeValueCountableConnection | null }; export type ProductTypesList_productTypes_ProductTypeCountableConnection_edges_ProductTypeCountableEdge_node_ProductType = { id: string, name: string, slug: string, hasVariants: boolean, productAttributes: Array | null }; @@ -33497,7 +33336,7 @@ export type ProductVariantDetail_productVariant_ProductVariant_assignedAttribute export type ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute_choices_AttributeValueCountableConnection = { edges: Array }; -export type ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute = { id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute_choices_AttributeValueCountableConnection | null }; +export type ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute = { id: string, name: string, slug: string, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute_choices_AttributeValueCountableConnection | null }; export type ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedFileAttribute_fileValue_File = { url: string, contentType: string | null }; @@ -33695,7 +33534,7 @@ export type VendorCustomerIdsVariables = Exact<{ export type VendorCustomerIds = VendorCustomerIds_Query; -export type VendorPageStatus_page_Page_attributes_SelectedAttribute_attribute_Attribute = { id: string, slug: string | null }; +export type VendorPageStatus_page_Page_attributes_SelectedAttribute_attribute_Attribute = { id: string, slug: string }; export type VendorPageStatus_page_Page_attributes_SelectedAttribute_values_AttributeValue_file_File = { url: string }; @@ -33715,7 +33554,7 @@ export type VendorPageStatusVariables = Exact<{ export type VendorPageStatus = VendorPageStatus_Query; -export type VendorPageType_pageTypes_PageTypeCountableConnection_edges_PageTypeCountableEdge_node_PageType_attributes_Attribute = { id: string, slug: string | null, inputType: AttributeInputTypeEnum | null }; +export type VendorPageType_pageTypes_PageTypeCountableConnection_edges_PageTypeCountableEdge_node_PageType_attributes_Attribute = { id: string, slug: string, inputType: AttributeInputTypeEnum | null }; export type VendorPageType_pageTypes_PageTypeCountableConnection_edges_PageTypeCountableEdge_node_PageType = { id: string, name: string, slug: string, attributes: Array | null }; diff --git a/apps/marketplace/src/lib/stripe/client.ts b/apps/marketplace/src/lib/stripe/client.ts index b7247339..bd3ae3f8 100644 --- a/apps/marketplace/src/lib/stripe/client.ts +++ b/apps/marketplace/src/lib/stripe/client.ts @@ -1,5 +1,3 @@ -import { APP_CONFIG } from "@/lib/saleor/consts"; - interface PaymentIntentCreateInput { amount: number; automatic_payment_methods: { diff --git a/apps/marketplace/src/lib/stripe/webhook-signature.ts b/apps/marketplace/src/lib/stripe/webhook-signature.ts index 40444dd8..5d06e417 100644 --- a/apps/marketplace/src/lib/stripe/webhook-signature.ts +++ b/apps/marketplace/src/lib/stripe/webhook-signature.ts @@ -15,38 +15,60 @@ const parseStripeSignature = (signature: string) => { }; }; -export const verifyStripeWebhookSignature = ({ +export type StripeWebhookVerifyFailureReason = + | "malformed_header_missing_timestamp_or_v1" + | "invalid_timestamp_number" + | "timestamp_outside_tolerance" + | "missing_signing_secret" + | "signature_mismatch"; + +export type StripeWebhookVerifyResult = + | { ok: true } + | { + /** Seconds between Stripe header timestamp and server time (absolute), if parsed */ + clockSkewSeconds?: number; + ok: false; + reason: StripeWebhookVerifyFailureReason; + }; + +/** + * Same as verifyStripeWebhookSignature but returns why verification failed (for debugging). + */ +export const verifyStripeWebhookSignatureDetailed = ({ payload, stripeSignature, }: { payload: string; stripeSignature: string; -}) => { +}): StripeWebhookVerifyResult => { const { timestamp, signatures } = parseStripeSignature(stripeSignature); if (!timestamp || signatures.length === 0) { - return false; + return { ok: false, reason: "malformed_header_missing_timestamp_or_v1" }; } const timestampNumber = Number(timestamp); if (!Number.isFinite(timestampNumber)) { - return false; + return { ok: false, reason: "invalid_timestamp_number" }; } const currentTimestamp = Math.floor(Date.now() / 1000); - - if ( - Math.abs(currentTimestamp - timestampNumber) > WEBHOOK_TOLERANCE_SECONDS - ) { - return false; + const clockSkewSeconds = Math.abs(currentTimestamp - timestampNumber); + + if (clockSkewSeconds > WEBHOOK_TOLERANCE_SECONDS) { + return { + clockSkewSeconds, + ok: false, + reason: "timestamp_outside_tolerance", + }; } const signedPayload = `${timestamp}.${payload}`; const secret = process.env.STRIPE_WEBHOOK_SIGNING_SECRET; if (!secret) { - throw new Error("Missing STRIPE_WEBHOOK_SIGNING_SECRET"); + return { ok: false, reason: "missing_signing_secret" }; } const expectedSignature = createHmac("sha256", secret) @@ -55,7 +77,7 @@ export const verifyStripeWebhookSignature = ({ const expectedBuffer = Buffer.from(expectedSignature, "hex"); - return signatures.some((signature) => { + const matched = signatures.some((signature) => { try { const signatureBuffer = Buffer.from(signature, "hex"); @@ -68,4 +90,29 @@ export const verifyStripeWebhookSignature = ({ return false; } }); + + if (!matched) { + return { + clockSkewSeconds, + ok: false, + reason: "signature_mismatch", + }; + } + + return { ok: true }; +}; + +export const verifyStripeWebhookSignature = ({ + payload, + stripeSignature, +}: { + payload: string; + stripeSignature: string; +}) => { + const result = verifyStripeWebhookSignatureDetailed({ + payload, + stripeSignature, + }); + + return result.ok; }; diff --git a/apps/storefront/src/app/[locale]/(checkout)/checkout/error.tsx b/apps/storefront/src/app/[locale]/(checkout)/checkout/error.tsx index 948fe05f..81baf3bf 100644 --- a/apps/storefront/src/app/[locale]/(checkout)/checkout/error.tsx +++ b/apps/storefront/src/app/[locale]/(checkout)/checkout/error.tsx @@ -2,6 +2,8 @@ import { useEffect, useState } from "react"; +import { isTransientRscNavigationError } from "@/foundation/errors/is-transient-rsc-navigation-error"; +import { RscStreamInterruptedFallback } from "@/foundation/errors/rsc-stream-interrupted-fallback"; import { errorService } from "@/services/error"; import { storefrontLogger } from "@/services/logging"; @@ -15,10 +17,23 @@ export default function Error({ const [traceId, setTraceId] = useState(null); useEffect(() => { + if (isTransientRscNavigationError(error)) { + storefrontLogger.debug( + "Checkout RSC stream aborted (usually harmless; navigation in flight)", + { message: error.message }, + ); + + return; + } + storefrontLogger.error("Checkout error", { error }); setTraceId(errorService.logError(error)); }, [error]); + if (isTransientRscNavigationError(error)) { + return ; + } + return (
diff --git a/apps/storefront/src/app/[locale]/(checkout)/order/confirmation/[id]/actions.ts b/apps/storefront/src/app/[locale]/(checkout)/order/confirmation/[id]/actions.ts deleted file mode 100644 index d9d8b2e6..00000000 --- a/apps/storefront/src/app/[locale]/(checkout)/order/confirmation/[id]/actions.ts +++ /dev/null @@ -1,14 +0,0 @@ -"use server"; - -import { cookies } from "next/headers"; - -import { COOKIE_KEY } from "@/config"; -import { storefrontLogger } from "@/services/logging"; - -export const clearCheckoutCookieAction = async () => { - const cookieStore = await cookies(); - - storefrontLogger.debug("Clearing checkout ID cookie on confirmation page."); - - cookieStore.delete(COOKIE_KEY.checkoutId); -}; diff --git a/apps/storefront/src/app/[locale]/(checkout)/order/confirmation/[id]/components/checkout-remover.tsx b/apps/storefront/src/app/[locale]/(checkout)/order/confirmation/[id]/components/checkout-remover.tsx deleted file mode 100644 index ded807e9..00000000 --- a/apps/storefront/src/app/[locale]/(checkout)/order/confirmation/[id]/components/checkout-remover.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import { useEffect } from "react"; - -import { useRouter } from "@nimara/i18n/routing"; - -import { paths, QUERY_PARAMS } from "@/foundation/routing/paths"; - -import { clearCheckoutCookieAction } from "../actions"; - -export const CheckoutRemover = ({ - params, - searchParams, -}: { - params: Promise<{ id: string }>; - searchParams: Promise<{ [QUERY_PARAMS.orderPlaced]: string }>; -}) => { - const router = useRouter(); - - useEffect(() => { - void (async () => { - if (QUERY_PARAMS.orderPlaced in (await searchParams)) { - const { id } = await params; - - await clearCheckoutCookieAction(); - - // After clearing the checkout cookie, we remove the `?orderPlaced=true` query param - // to prevent the action from being triggered again on page reload. - router.replace(paths.order.confirmation.asPath({ id })); - } - })(); - }, []); - - return null; -}; diff --git a/apps/storefront/src/app/[locale]/(checkout)/order/confirmation/[id]/page.tsx b/apps/storefront/src/app/[locale]/(checkout)/order/confirmation/[id]/page.tsx index a7ddb1bc..2c59b330 100644 --- a/apps/storefront/src/app/[locale]/(checkout)/order/confirmation/[id]/page.tsx +++ b/apps/storefront/src/app/[locale]/(checkout)/order/confirmation/[id]/page.tsx @@ -4,13 +4,10 @@ import { getTranslations } from "next-intl/server"; import { LocalizedLink } from "@nimara/i18n/routing"; import { Button } from "@nimara/ui/components/button"; -import { paths, type QUERY_PARAMS } from "@/foundation/routing/paths"; - -import { CheckoutRemover } from "./components/checkout-remover"; +import { paths } from "@/foundation/routing/paths"; type PageProps = { params: Promise<{ id: string; locale: Locale }>; - searchParams: Promise<{ [QUERY_PARAMS.orderPlaced]: string }>; }; export async function generateMetadata() { @@ -21,7 +18,7 @@ export async function generateMetadata() { }; } -export default async function Page({ params, searchParams }: PageProps) { +export default async function Page(_props: PageProps) { const t = await getTranslations(); return ( @@ -37,7 +34,6 @@ export default async function Page({ params, searchParams }: PageProps) { {t("common.back-to-homepage")} -
); } diff --git a/apps/storefront/src/app/[locale]/(main)/error.tsx b/apps/storefront/src/app/[locale]/(main)/error.tsx index 03e4c52c..43af5243 100644 --- a/apps/storefront/src/app/[locale]/(main)/error.tsx +++ b/apps/storefront/src/app/[locale]/(main)/error.tsx @@ -2,6 +2,8 @@ import { useEffect, useState } from "react"; +import { isTransientRscNavigationError } from "@/foundation/errors/is-transient-rsc-navigation-error"; +import { RscStreamInterruptedFallback } from "@/foundation/errors/rsc-stream-interrupted-fallback"; import { errorService } from "@/services/error"; import { storefrontLogger } from "@/services/logging"; @@ -15,11 +17,24 @@ export default function Error({ const [traceId, setTraceId] = useState(null); useEffect(() => { + if (isTransientRscNavigationError(error)) { + storefrontLogger.debug( + "RSC stream aborted (usually harmless; navigation in flight)", + { message: error.message }, + ); + + return; + } + storefrontLogger.error("Unexpected error", { error }); setTraceId(errorService.logError(error)); }, [error]); + if (isTransientRscNavigationError(error)) { + return ; + } + return (
diff --git a/apps/storefront/src/foundation/checkout/actions/update-checkout-address-action.ts b/apps/storefront/src/foundation/checkout/actions/update-checkout-address-action.ts index 4d7b0fd6..74138899 100644 --- a/apps/storefront/src/foundation/checkout/actions/update-checkout-address-action.ts +++ b/apps/storefront/src/foundation/checkout/actions/update-checkout-address-action.ts @@ -22,10 +22,16 @@ import { getServiceRegistry } from "@/services/registry"; */ export const updateCheckoutAddressAction = async ({ type, + revalidateCheckout = true, ...values }: { address: Partial; id: Checkout["id"]; + /** + * When false, skips revalidatePath (e.g. immediately before Stripe redirect). + * @see updateBillingAddress in payment/actions.ts + */ + revalidateCheckout?: boolean; type: AddressType; }): AsyncResult<{ success: true; @@ -64,7 +70,9 @@ export const updateCheckoutAddressAction = async ({ } } - revalidatePath(paths.checkout.asPath()); + if (revalidateCheckout) { + revalidatePath(paths.checkout.asPath()); + } return ok({ success: true }); }; diff --git a/apps/storefront/src/foundation/checkout/order-placed-cleanup-middleware.ts b/apps/storefront/src/foundation/checkout/order-placed-cleanup-middleware.ts new file mode 100644 index 00000000..e1cfd5ae --- /dev/null +++ b/apps/storefront/src/foundation/checkout/order-placed-cleanup-middleware.ts @@ -0,0 +1,49 @@ +import { + type NextFetchEvent, + type NextRequest, + NextResponse, +} from "next/server"; + +import { type CustomMiddleware } from "@nimara/foundation/middleware/chain"; + +import { COOKIE_KEY } from "@/config"; + +/** Must match `QUERY_PARAMS.orderPlaced` in `@/foundation/routing/paths`. */ +const ORDER_PLACED_QUERY = "orderPlaced"; + +/** + * After successful payment, users land on order confirmation with `?orderPlaced=true`. + * Clears checkout cookie and strips the query via redirect on the same pathname. + * Runs in middleware so cookie mutation is allowed and locale-prefixed URLs stay intact. + * Skips prefetch to avoid breaking RSC prefetch and spurious cookie deletion. + */ +export function orderPlacedCleanupMiddleware( + next: CustomMiddleware, +): CustomMiddleware { + return async ( + request: NextRequest, + event: NextFetchEvent, + prevResponse: NextResponse, + ) => { + const isPrefetch = request.headers.get("x-nextjs-prefetch") === "1"; + + if ( + request.method !== "GET" || + isPrefetch || + !request.nextUrl.searchParams.has(ORDER_PLACED_QUERY) || + !request.nextUrl.pathname.includes("/order/confirmation/") + ) { + return next(request, event, prevResponse); + } + + const url = request.nextUrl.clone(); + + url.searchParams.delete(ORDER_PLACED_QUERY); + + const response = NextResponse.redirect(url); + + response.cookies.delete(COOKIE_KEY.checkoutId); + + return response; + }; +} diff --git a/apps/storefront/src/foundation/checkout/sections/payment/actions.ts b/apps/storefront/src/foundation/checkout/sections/payment/actions.ts index eaa0e277..8f7ecb39 100644 --- a/apps/storefront/src/foundation/checkout/sections/payment/actions.ts +++ b/apps/storefront/src/foundation/checkout/sections/payment/actions.ts @@ -1,7 +1,5 @@ "use server"; -import { revalidatePath } from "next/cache"; - import { type AllCountryCode } from "@nimara/domain/consts"; import { type Checkout } from "@nimara/domain/objects/Checkout"; import { type AsyncResult, err, ok } from "@nimara/domain/objects/Result"; @@ -10,7 +8,6 @@ import { schemaToAddress } from "@nimara/foundation/address/address"; import { clientEnvs } from "@/envs/client"; import { createAddressAction } from "@/foundation/address/create-address-action"; import { updateCheckoutAddressAction } from "@/foundation/checkout/actions/update-checkout-address-action"; -import { paths } from "@/foundation/routing/paths"; import { storefrontLogger } from "@/services/logging"; import { getServiceRegistry } from "@/services/registry"; import { getAccessToken } from "@/services/tokens"; @@ -20,12 +17,19 @@ import { type PaymentSchema } from "./schema"; export async function updateBillingAddress({ checkout, input: { sameAsShippingAddress, billingAddress, saveAddressForFutureUse }, + /** + * When false, skips revalidatePath after a successful update. Use before Stripe + * confirmPayment + redirect so RSC refetch does not race with navigation (Next.js + * "Error in input stream"). Call router.refresh() on the client if payment fails. + */ + revalidateCheckout = true, }: { checkout: Checkout; input: Pick< PaymentSchema, "sameAsShippingAddress" | "billingAddress" | "saveAddressForFutureUse" >; + revalidateCheckout?: boolean; }) { const result = await updateCheckoutAddressAction({ id: checkout.id, @@ -33,6 +37,7 @@ export async function updateBillingAddress({ ? checkout.shippingAddress! : schemaToAddress(billingAddress!), type: "BILLING", + revalidateCheckout, }); if (saveAddressForFutureUse) { @@ -54,10 +59,6 @@ export async function updateBillingAddress({ } } - if (result.ok) { - revalidatePath(paths.checkout.asPath()); - } - return result; } diff --git a/apps/storefront/src/foundation/checkout/sections/payment/payment.tsx b/apps/storefront/src/foundation/checkout/sections/payment/payment.tsx index ec659752..e63e0984 100644 --- a/apps/storefront/src/foundation/checkout/sections/payment/payment.tsx +++ b/apps/storefront/src/foundation/checkout/sections/payment/payment.tsx @@ -263,6 +263,7 @@ export const Payment = ({ saveAddressForFutureUse, billingAddress, }, + revalidateCheckout: false, }); if (!result.ok) { @@ -304,6 +305,7 @@ export const Payment = ({ if (!result.ok) { setErrors(result.errors.map(({ code }) => code)); setIsProcessing(false); + router.refresh(); return; } @@ -323,6 +325,7 @@ export const Payment = ({ if (!result.ok) { setErrors(result.errors.map(({ code }) => code)); setIsProcessing(false); + router.refresh(); } }; diff --git a/apps/storefront/src/foundation/errors/is-transient-rsc-navigation-error.ts b/apps/storefront/src/foundation/errors/is-transient-rsc-navigation-error.ts new file mode 100644 index 00000000..afccb5f1 --- /dev/null +++ b/apps/storefront/src/foundation/errors/is-transient-rsc-navigation-error.ts @@ -0,0 +1,11 @@ +/** + * Next.js / React may throw when an RSC Flight fetch is aborted by navigation + * (e.g. Stripe return_url redirect). Not a user-facing failure — order can still succeed. + */ +export function isTransientRscNavigationError(error: unknown): boolean { + return ( + error instanceof Error && + (error.message === "Error in input stream" || + error.message.includes("Error in input stream")) + ); +} diff --git a/apps/storefront/src/foundation/errors/rsc-stream-interrupted-fallback.tsx b/apps/storefront/src/foundation/errors/rsc-stream-interrupted-fallback.tsx new file mode 100644 index 00000000..814fb31f --- /dev/null +++ b/apps/storefront/src/foundation/errors/rsc-stream-interrupted-fallback.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { Spinner } from "@nimara/ui/components/spinner"; + +/** Matches checkout main area (`bg-muted`) so brief RSC aborts do not flash a white error screen. */ +export function RscStreamInterruptedFallback() { + return ( +
+ +
+ ); +} diff --git a/apps/storefront/src/proxy.ts b/apps/storefront/src/proxy.ts index d027389c..6cc0d574 100644 --- a/apps/storefront/src/proxy.ts +++ b/apps/storefront/src/proxy.ts @@ -1,9 +1,14 @@ import { chain } from "@nimara/foundation/middleware/chain"; import { authMiddleware } from "@/foundation/auth/authMiddleware"; +import { orderPlacedCleanupMiddleware } from "@/foundation/checkout/order-placed-cleanup-middleware"; import { i18nMiddleware } from "@/foundation/i18n/middleware"; -export default chain([i18nMiddleware, authMiddleware]); +export default chain([ + orderPlacedCleanupMiddleware, + i18nMiddleware, + authMiddleware, +]); export const config = { matcher: [ diff --git a/apps/stripe/src/graphql/generated/client.ts b/apps/stripe/src/graphql/generated/client.ts index f27ae090..1eaf4023 100644 --- a/apps/stripe/src/graphql/generated/client.ts +++ b/apps/stripe/src/graphql/generated/client.ts @@ -268,6 +268,7 @@ export type AccountErrorCode = | "DELETE_OWN_ACCOUNT" | "DELETE_STAFF_ACCOUNT" | "DELETE_SUPERUSER_ACCOUNT" + | "DISABLED_AUTHENTICATION_METHOD" | "DUPLICATED_INPUT_ITEM" | "GRAPHQL_ERROR" | "INACTIVE" @@ -1048,38 +1049,21 @@ export type AppExtension = Node & { /** Label of the extension to show in the dashboard. */ label: Scalars["String"]["output"]; /** - * Place where given extension will be mounted. - * @deprecated Use `mountName` instead. - */ - mount: AppExtensionMountEnum; - /** - * Name of the extension mount point in the dashboard. Replaces `mount` + * Name of the extension mount point in the dashboard. Value returned in UPPERCASE. * * Added in Saleor 3.22. */ mountName: Scalars["String"]["output"]; - /** - * App extension options. - * - * Added in Saleor 3.22. - * @deprecated Use `settings` field instead. - */ - options: Maybe; /** List of the app extension's permissions. */ permissions: Array; /** - * App extension settings. Replaces `options` field. + * App extension settings. * * Added in Saleor 3.22. */ settings: Scalars["JSON"]["output"]; /** - * Type of way how app extension will be opened. - * @deprecated Use `targetName` instead. - */ - target: AppExtensionTargetEnum; - /** - * Name of the extension target in the dashboard. Replaces `target` + * Name of the extension target in the dashboard. Value returned in UPPERCASE. * * Added in Saleor 3.22. */ @@ -1104,24 +1088,12 @@ export type AppExtensionCountableEdge = { }; export type AppExtensionFilterInput = { - /** - * DEPRECATED: Use `mountName` instead. - * - * DEPRECATED: this field will be removed. - */ - mount?: InputMaybe>; /** * Plain-text mount name (case insensitive) * * Added in Saleor 3.22. */ mountName?: InputMaybe>; - /** - * DEPRECATED: Use `targetName` instead. - * - * DEPRECATED: this field will be removed. - */ - target?: InputMaybe; /** * Plain-text target name (case insensitive) * @@ -1130,94 +1102,6 @@ export type AppExtensionFilterInput = { targetName?: InputMaybe; }; -/** All places where app extension can be mounted. */ -export type AppExtensionMountEnum = - | "CATEGORY_DETAILS_MORE_ACTIONS" - | "CATEGORY_OVERVIEW_CREATE" - | "CATEGORY_OVERVIEW_MORE_ACTIONS" - | "COLLECTION_DETAILS_MORE_ACTIONS" - | "COLLECTION_DETAILS_WIDGETS" - | "COLLECTION_OVERVIEW_CREATE" - | "COLLECTION_OVERVIEW_MORE_ACTIONS" - | "CUSTOMER_DETAILS_MORE_ACTIONS" - | "CUSTOMER_DETAILS_WIDGETS" - | "CUSTOMER_OVERVIEW_CREATE" - | "CUSTOMER_OVERVIEW_MORE_ACTIONS" - | "DISCOUNT_DETAILS_MORE_ACTIONS" - | "DISCOUNT_OVERVIEW_CREATE" - | "DISCOUNT_OVERVIEW_MORE_ACTIONS" - | "DRAFT_ORDER_DETAILS_MORE_ACTIONS" - | "DRAFT_ORDER_DETAILS_WIDGETS" - | "DRAFT_ORDER_OVERVIEW_CREATE" - | "DRAFT_ORDER_OVERVIEW_MORE_ACTIONS" - | "GIFT_CARD_DETAILS_MORE_ACTIONS" - | "GIFT_CARD_DETAILS_WIDGETS" - | "GIFT_CARD_OVERVIEW_CREATE" - | "GIFT_CARD_OVERVIEW_MORE_ACTIONS" - | "MENU_DETAILS_MORE_ACTIONS" - | "MENU_OVERVIEW_CREATE" - | "MENU_OVERVIEW_MORE_ACTIONS" - | "NAVIGATION_CATALOG" - | "NAVIGATION_CUSTOMERS" - | "NAVIGATION_DISCOUNTS" - | "NAVIGATION_ORDERS" - | "NAVIGATION_PAGES" - | "NAVIGATION_TRANSLATIONS" - | "ORDER_DETAILS_MORE_ACTIONS" - | "ORDER_DETAILS_WIDGETS" - | "ORDER_OVERVIEW_CREATE" - | "ORDER_OVERVIEW_MORE_ACTIONS" - | "PAGE_DETAILS_MORE_ACTIONS" - | "PAGE_OVERVIEW_CREATE" - | "PAGE_OVERVIEW_MORE_ACTIONS" - | "PAGE_TYPE_DETAILS_MORE_ACTIONS" - | "PAGE_TYPE_OVERVIEW_CREATE" - | "PAGE_TYPE_OVERVIEW_MORE_ACTIONS" - | "PRODUCT_DETAILS_MORE_ACTIONS" - | "PRODUCT_DETAILS_WIDGETS" - | "PRODUCT_OVERVIEW_CREATE" - | "PRODUCT_OVERVIEW_MORE_ACTIONS" - | "TRANSLATIONS_MORE_ACTIONS" - | "VOUCHER_DETAILS_MORE_ACTIONS" - | "VOUCHER_DETAILS_WIDGETS" - | "VOUCHER_OVERVIEW_CREATE" - | "VOUCHER_OVERVIEW_MORE_ACTIONS"; - -/** Represents the options for an app extension. */ -export type AppExtensionOptionsNewTab = { - /** - * Options controlling behavior of the NEW_TAB extension target - * @deprecated Use `settings` field directly. - */ - newTabTarget: Maybe; -}; - -/** Represents the options for an app extension. */ -export type AppExtensionOptionsWidget = { - /** - * Options for displaying a Widget - * @deprecated Use `settings` field directly. - */ - widgetTarget: Maybe; -}; - -export type AppExtensionPossibleOptions = - | AppExtensionOptionsNewTab - | AppExtensionOptionsWidget; - -/** - * All available ways of opening an app extension. - * - * POPUP - app's extension will be mounted as a popup window - * APP_PAGE - redirect to app's page - * - */ -export type AppExtensionTargetEnum = - | "APP_PAGE" - | "NEW_TAB" - | "POPUP" - | "WIDGET"; - /** * Fetch and validate manifest. * @@ -1262,9 +1146,9 @@ export type AppInstallInput = { /** Determine if app will be set active or not. */ activateAfterInstallation?: InputMaybe; /** Name of the app to install. */ - appName?: InputMaybe; + appName: Scalars["String"]["input"]; /** URL to app's manifest in JSON format. */ - manifestUrl?: InputMaybe; + manifestUrl: Scalars["String"]["input"]; /** List of permission code names to assign to this app. */ permissions?: InputMaybe>; }; @@ -1326,12 +1210,7 @@ export type AppManifestExtension = { /** Label of the extension to show in the dashboard. */ label: Scalars["String"]["output"]; /** - * Place where given extension will be mounted. - * @deprecated Use `mountName` instead. - */ - mount: AppExtensionMountEnum; - /** - * Name of the extension mount point in the dashboard. Replaces `mount` + * Name of the extension mount point in the dashboard. Value returned in UPPERCASE. * * Added in Saleor 3.22. */ @@ -1339,18 +1218,13 @@ export type AppManifestExtension = { /** List of the app extension's permissions. */ permissions: Array; /** - * JSON object with settings for this extension. + * App extension settings. * * Added in Saleor 3.22. */ settings: Scalars["JSON"]["output"]; /** - * Type of way how app extension will be opened. - * @deprecated Use `targetName` instead. - */ - target: AppExtensionTargetEnum; - /** - * Name of the extension target in the dashboard. Replaces `target` + * Name of the extension target in the dashboard. Value returned in UPPERCASE. * * Added in Saleor 3.22. */ @@ -2185,7 +2059,7 @@ export type Attribute = Node & /** Public metadata. Use `keys` to control which fields you want to include. The default is to include everything. */ metafields: Maybe; /** Name of an attribute displayed in the interface. */ - name: Maybe; + name: Scalars["String"]["output"]; /** List of private metadata items. Requires staff permissions to access. */ privateMetadata: Array; /** @@ -2207,7 +2081,7 @@ export type Attribute = Node & */ referenceTypes: Maybe>; /** Internal representation of an attribute name. */ - slug: Maybe; + slug: Scalars["String"]["output"]; /** * The position of the attribute in the storefront navigation (0 by default). Requires one of the following permissions: MANAGE_PAGES, MANAGE_PAGE_TYPES_AND_ATTRIBUTES, MANAGE_PRODUCTS, MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES. * @deprecated No longer supported @@ -2216,7 +2090,7 @@ export type Attribute = Node & /** Returns translated attribute fields for the given language code. */ translation: Maybe; /** The attribute type. */ - type: Maybe; + type: AttributeTypeEnum; /** The unit of attribute values. */ unit: Maybe; /** Whether the attribute requires values to be passed or not. Requires one of the following permissions: MANAGE_PAGES, MANAGE_PAGE_TYPES_AND_ATTRIBUTES, MANAGE_PRODUCTS, MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES. */ @@ -4325,12 +4199,19 @@ export type Checkout = Node & * Added in Saleor 3.21. */ customerNote: Scalars["String"]["output"]; + /** + * The delivery method selected for this checkout. + * + * Added in Saleor 3.23. + */ + delivery: Maybe; /** * The delivery method selected for this checkout. * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Optionally triggered when cached external shipping methods are invalid. * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. + * @deprecated Use `delivery` instead. */ deliveryMethod: Maybe; /** The total discount applied to the checkout. Note: Only discount created via voucher are included in this field. */ @@ -4390,7 +4271,7 @@ export type Checkout = Node & * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Optionally triggered when cached external shipping methods are invalid. * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. - * @deprecated Use `deliveryMethod` instead. + * @deprecated Use `delivery` instead. */ shippingMethod: Maybe; /** @@ -4788,6 +4669,7 @@ export type CheckoutCustomerNoteUpdate = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ export type CheckoutDeliveryMethodUpdate = { @@ -5187,7 +5069,27 @@ export type CheckoutPaymentCreate = { /** Represents an problem in the checkout. */ export type CheckoutProblem = | CheckoutLineProblemInsufficientStock - | CheckoutLineProblemVariantNotAvailable; + | CheckoutLineProblemVariantNotAvailable + | CheckoutProblemDeliveryMethodInvalid + | CheckoutProblemDeliveryMethodStale; + +/** + * Indicates that the selected delivery method is invalid. + * + * Added in Saleor 3.23. + */ +export type CheckoutProblemDeliveryMethodInvalid = { + delivery: Delivery; +}; + +/** + * Indicates that the delivery methods are stale. + * + * Added in Saleor 3.23. + */ +export type CheckoutProblemDeliveryMethodStale = { + delivery: Delivery; +}; /** * Remove a gift card or a voucher from a checkout. @@ -5205,6 +5107,12 @@ export type CheckoutRemovePromoCode = { /** Represents the channel-specific checkout settings. */ export type CheckoutSettings = { + /** + * Default to `true`. Determines whether gift cards can be attached to a Checkout via `addPromoCode` mutation. Usage of this mutation with gift cards is deprecated. + * + * Added in Saleor 3.23. + */ + allowLegacyGiftCardUse: Scalars["Boolean"]["output"]; /** * The date time defines the earliest checkout creation date on which fully paid checkouts can begin to be automatically completed. * @@ -5232,6 +5140,12 @@ export type CheckoutSettings = { }; export type CheckoutSettingsInput = { + /** + * Default to `true`. Determines whether gift cards can be attached to a Checkout via `addPromoCode` mutation. Usage of this mutation with gift cards is deprecated. + * + * Added in Saleor 3.23. + */ + allowLegacyGiftCardUse?: InputMaybe; /** * Settings for automatic completion of fully paid checkouts. * @@ -5275,6 +5189,7 @@ export type CheckoutShippingAddressUpdate = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout shipping method with the external one. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ export type CheckoutShippingMethodUpdate = { @@ -5291,7 +5206,9 @@ export type CheckoutSortField = /** Sort checkouts by customer. */ | "CUSTOMER" /** Sort checkouts by payment. */ - | "PAYMENT"; + | "PAYMENT" + /** Sort checkouts by rank. Note: This option is available only with the `search` filter. */ + | "RANK"; export type CheckoutSortingInput = { /** Specifies the direction in which to sort checkouts. */ @@ -6702,8 +6619,6 @@ export type CustomerEvent = Node & { message: Maybe; /** The concerned order. */ order: Maybe; - /** The concerned order line. */ - orderLine: Maybe; /** Customer event type. */ type: Maybe; /** User who performed the action. */ @@ -6969,205 +6884,50 @@ export type DeletePrivateMetadata = { metadataErrors: Array; }; -/** Represents a delivery method chosen for the checkout. `Warehouse` type is used when checkout is marked as "click and collect" and `ShippingMethod` otherwise. */ -export type DeliveryMethod = ShippingMethod | Warehouse; - -/** Represents digital content associated with a product variant. */ -export type DigitalContent = Node & - ObjectWithMetadata & { - /** Indicator for automatic fulfillment of digital content. */ - automaticFulfillment: Scalars["Boolean"]["output"]; - /** File associated with digital content. */ - contentFile: Scalars["String"]["output"]; - /** The ID of the digital content. */ - id: Scalars["ID"]["output"]; - /** Maximum number of allowed downloads for the digital content. */ - maxDownloads: Maybe; - /** List of public metadata items. Can be accessed without permissions. */ - metadata: Array; - /** - * A single key from public metadata. - * - * Tip: Use GraphQL aliases to fetch multiple keys. - */ - metafield: Maybe; - /** Public metadata. Use `keys` to control which fields you want to include. The default is to include everything. */ - metafields: Maybe; - /** List of private metadata items. Requires staff permissions to access. */ - privateMetadata: Array; - /** - * A single key from private metadata. Requires staff permissions to access. - * - * Tip: Use GraphQL aliases to fetch multiple keys. - */ - privateMetafield: Maybe; - /** Private metadata. Requires staff permissions to access. Use `keys` to control which fields you want to include. The default is to include everything. */ - privateMetafields: Maybe; - /** Product variant assigned to digital content. */ - productVariant: ProductVariant; - /** Number of days the URL for the digital content remains valid. */ - urlValidDays: Maybe; - /** List of URLs for the digital variant. */ - urls: Maybe>; - /** Default settings indicator for digital content. */ - useDefaultSettings: Scalars["Boolean"]["output"]; - }; - -/** Represents digital content associated with a product variant. */ -export type DigitalContentMetafieldArgs = { - key: Scalars["String"]["input"]; -}; - -/** Represents digital content associated with a product variant. */ -export type DigitalContentMetafieldsArgs = { - keys?: InputMaybe>; -}; - -/** Represents digital content associated with a product variant. */ -export type DigitalContentPrivateMetafieldArgs = { - key: Scalars["String"]["input"]; -}; - -/** Represents digital content associated with a product variant. */ -export type DigitalContentPrivateMetafieldsArgs = { - keys?: InputMaybe>; -}; - -/** A connection to a list of digital content items. */ -export type DigitalContentCountableConnection = { - edges: Array; - /** Pagination data for this connection. */ - pageInfo: PageInfo; - /** A total count of items in the collection. */ - totalCount: Maybe; -}; - -export type DigitalContentCountableEdge = { - /** A cursor for use in pagination. */ - cursor: Scalars["String"]["output"]; - /** The item at the end of the edge. */ - node: DigitalContent; -}; - /** - * Create new digital content. This mutation must be sent as a `multipart` request. More detailed specs of the upload format can be found here: https://github.com/jaydenseric/graphql-multipart-request-spec + * Represents a delivery option for the checkout. * - * Requires one of the following permissions: MANAGE_PRODUCTS. + * Added in Saleor 3.23. */ -export type DigitalContentCreate = { - content: Maybe; - errors: Array; - /** @deprecated Use `errors` field instead. */ - productErrors: Array; - variant: Maybe; -}; - -/** - * Remove digital content assigned to given variant. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - */ -export type DigitalContentDelete = { - errors: Array; - /** @deprecated Use `errors` field instead. */ - productErrors: Array; - variant: Maybe; +export type Delivery = { + /** The ID of the delivery. */ + id: Scalars["ID"]["output"]; + /** Shipping method represented by the delivery. */ + shippingMethod: Maybe; }; -export type DigitalContentInput = { - /** Overwrite default automatic_fulfillment setting for variant. */ - automaticFulfillment?: InputMaybe; - /** Determines how many times a download link can be accessed by a customer. */ - maxDownloads?: InputMaybe; - /** - * Fields required to update the digital content metadata. Can be read by any API client authorized to read the object it's attached to. - * - * Warning: never store sensitive information, including financial data such as credit card details. - */ - metadata?: InputMaybe>; - /** - * Fields required to update the digital content private metadata. Requires permissions to modify and to read the metadata of the object it's attached to. - * - * Warning: never store sensitive information, including financial data such as credit card details. - */ - privateMetadata?: InputMaybe>; - /** Determines for how many days a download link is active since it was generated. */ - urlValidDays?: InputMaybe; - /** Use default digital content settings for this product. */ - useDefaultSettings: Scalars["Boolean"]["input"]; -}; +/** Represents a delivery method chosen for the checkout. `Warehouse` type is used when checkout is marked as "click and collect" and `ShippingMethod` otherwise. */ +export type DeliveryMethod = ShippingMethod | Warehouse; /** - * Updates digital content. + * Calculates available delivery options for a checkout. * - * Requires one of the following permissions: MANAGE_PRODUCTS. - */ -export type DigitalContentUpdate = { - content: Maybe; - errors: Array; - /** @deprecated Use `errors` field instead. */ - productErrors: Array; - variant: Maybe; -}; - -export type DigitalContentUploadInput = { - /** Overwrite default automatic_fulfillment setting for variant. */ - automaticFulfillment?: InputMaybe; - /** Represents an file in a multipart request. */ - contentFile: Scalars["Upload"]["input"]; - /** Determines how many times a download link can be accessed by a customer. */ - maxDownloads?: InputMaybe; - /** - * Fields required to update the digital content metadata. Can be read by any API client authorized to read the object it's attached to. - * - * Warning: never store sensitive information, including financial data such as credit card details. - */ - metadata?: InputMaybe>; - /** - * Fields required to update the digital content private metadata. Requires permissions to modify and to read the metadata of the object it's attached to. - * - * Warning: never store sensitive information, including financial data such as credit card details. - */ - privateMetadata?: InputMaybe>; - /** Determines for how many days a download link is active since it was generated. */ - urlValidDays?: InputMaybe; - /** Use default digital content settings for this product. */ - useDefaultSettings: Scalars["Boolean"]["input"]; -}; - -/** Represents a URL for digital content. */ -export type DigitalContentUrl = Node & { - /** Digital content associated with the URL. */ - content: DigitalContent; - /** Date and time when the digital content URL was created. */ - created: Scalars["DateTime"]["output"]; - /** Number of times digital content has been downloaded. */ - downloadNum: Scalars["Int"]["output"]; - /** The ID of the digital content URL. */ - id: Scalars["ID"]["output"]; - /** UUID of digital content. */ - token: Scalars["UUID"]["output"]; - /** URL for digital content. */ - url: Maybe; -}; - -/** - * Generate new URL to digital content. + * Added in Saleor 3.23. * - * Requires one of the following permissions: MANAGE_PRODUCTS. + * Triggers the following webhook events: + * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered to fetch external shipping methods. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Triggered to filter shipping methods. */ -export type DigitalContentUrlCreate = { - digitalContentUrl: Maybe; - errors: Array; - /** @deprecated Use `errors` field instead. */ - productErrors: Array; +export type DeliveryOptionsCalculate = { + /** List of the available deliveries. */ + deliveries: Array; + errors: Array; }; -export type DigitalContentUrlCreateInput = { - /** Digital content ID which URL will belong to. */ - content: Scalars["ID"]["input"]; +export type DeliveryOptionsCalculateError = { + /** The error code. */ + code: DeliveryOptionsCalculateErrorCode; + /** Name of a field that caused the error. A value of `null` indicates that the error isn't associated with a particular field. */ + field: Maybe; + /** The error message. */ + message: Maybe; }; +export type DeliveryOptionsCalculateErrorCode = + | "GRAPHQL_ERROR" + | "INVALID" + | "NOT_FOUND"; + export type DiscountError = { /** List of channels IDs which causes the error. */ channels: Maybe>; @@ -7332,7 +7092,11 @@ export type DraftOrderCreateInput = { user?: InputMaybe; /** Email address of the customer. */ userEmail?: InputMaybe; - /** ID of the voucher associated with the order. */ + /** + * ID of the voucher associated with the order. + * + * DEPRECATED: this field will be removed. Use `voucherCode` instead. + */ voucher?: InputMaybe; /** * A code of the voucher associated with the order. @@ -7441,7 +7205,11 @@ export type DraftOrderInput = { user?: InputMaybe; /** Email address of the customer. */ userEmail?: InputMaybe; - /** ID of the voucher associated with the order. */ + /** + * ID of the voucher associated with the order. + * + * DEPRECATED: this field will be removed. Use `voucherCode` instead. + */ voucher?: InputMaybe; /** * A code of the voucher associated with the order. @@ -7852,8 +7620,6 @@ export type ExportScope = * * Added in Saleor 3.18. * - * Note: this API is currently in Feature Preview and can be subject to changes at later point. - * * Requires one of the following permissions: MANAGE_DISCOUNTS. * * Triggers the following webhook events: @@ -8330,7 +8096,7 @@ export type GiftCard = Node & */ endDate: Maybe; /** - * List of events associated with the gift card. Requires MANAGE_GIFT_CARD permission to access all events. Users with MANAGE_ORDERS permission can access only USED_IN_ORDER events. + * List of events associated with the gift card. Requires MANAGE_GIFT_CARD permission to access all events. Users with MANAGE_ORDERS permission can access only USED_IN_ORDER and REFUNDED_IN_ORDER events. * * Requires one of the following permissions: MANAGE_GIFT_CARD, MANAGE_ORDERS. */ @@ -8747,6 +8513,7 @@ export type GiftCardEventsEnum = | "EXPIRY_DATE_UPDATED" | "ISSUED" | "NOTE_ADDED" + | "REFUNDED_IN_ORDER" | "RESENT" | "SENT_TO_CUSTOMER" | "TAGS_UPDATED" @@ -8795,6 +8562,55 @@ export type GiftCardMetadataUpdated = Event & { version: Maybe; }; +/** + * Represents a gift card payment method used for a transaction. + * + * Added in Saleor 3.23. + */ +export type GiftCardPaymentMethodDetails = PaymentMethodDetails & { + /** + * Brand of the gift card. + * + * Added in Saleor 3.23. + */ + brand: Maybe; + /** + * Indicates whether the gift card is a built-in Saleor gift card. + * + * Added in Saleor 3.23. + */ + isSaleorGiftcard: Scalars["Boolean"]["output"]; + /** + * Last characters of the gift card code. Max 4 characters. + * + * Added in Saleor 3.23. + */ + lastChars: Maybe; + /** Name of the gift card. */ + name: Scalars["String"]["output"]; +}; + +export type GiftCardPaymentMethodDetailsInput = { + /** + * Brand of the gift card used for the transaction. Max length is 40 characters. + * + * Added in Saleor 3.23. + */ + brand?: InputMaybe; + /** + * Last characters of the gift card used for the transaction. Max length is 4 characters. + * + * Added in Saleor 3.23. + */ + lastChars?: InputMaybe; + /** + * Name of the payment method used for the transaction. Max length is 256 characters. + * + * Added in Saleor 3.23. + */ + name: Scalars["String"]["input"]; +}; + /** * Resend a gift card. * @@ -8885,6 +8701,8 @@ export type GiftCardSortField = | "CURRENT_BALANCE" /** Sort gift cards by product. */ | "PRODUCT" + /** Sort gift cards by rank. Note: This option is available only with the `search` filter. */ + | "RANK" /** Sort gift cards by used by. */ | "USED_BY"; @@ -9049,8 +8867,6 @@ export type GroupCountableEdge = { node: Group; }; -export type HttpMethod = "GET" | "POST"; - /** Thumbnail formats for icon images. */ export type IconThumbnailFormatEnum = "ORIGINAL" | "WEBP"; @@ -12250,6 +12066,7 @@ export type Mutation = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ checkoutDeliveryMethodUpdate: Maybe; @@ -12317,6 +12134,7 @@ export type Mutation = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout shipping method with the external one. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. * @deprecated Use `checkoutDeliveryMethodUpdate` instead. */ @@ -12460,33 +12278,15 @@ export type Mutation = { */ deleteWarehouse: Maybe; /** - * Create new digital content. This mutation must be sent as a `multipart` request. More detailed specs of the upload format can be found here: https://github.com/jaydenseric/graphql-multipart-request-spec - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContentCreate: Maybe; - /** - * Remove digital content assigned to given variant. + * Calculates available delivery options for a checkout. * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContentDelete: Maybe; - /** - * Updates digital content. + * Added in Saleor 3.23. * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContentUpdate: Maybe; - /** - * Generate new URL to digital content. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + * Triggers the following webhook events: + * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered to fetch external shipping methods. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Triggered to filter shipping methods. */ - digitalContentUrlCreate: Maybe; + deliveryOptionsCalculate: Maybe; /** * Deletes draft orders. * @@ -12538,6 +12338,7 @@ export type Mutation = { * Triggers the following webhook events: * - NOTIFY_USER (async): A notification for the exported file. * - GIFT_CARD_EXPORT_COMPLETED (async): A notification for the exported file. + * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportGiftCards: Maybe; /** @@ -12548,6 +12349,7 @@ export type Mutation = { * Triggers the following webhook events: * - NOTIFY_USER (async): A notification for the exported file. * - PRODUCT_EXPORT_COMPLETED (async): A notification for the exported file. + * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportProducts: Maybe; /** @@ -12555,12 +12357,11 @@ export type Mutation = { * * Added in Saleor 3.18. * - * Note: this API is currently in Feature Preview and can be subject to changes at later point. - * * Requires one of the following permissions: MANAGE_DISCOUNTS. * * Triggers the following webhook events: * - VOUCHER_CODE_EXPORT_COMPLETED (async): A notification for the exported file. + * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportVoucherCodes: Maybe; /** Prepare external authentication URL for user by custom plugin. */ @@ -14567,22 +14368,8 @@ export type MutationDeleteWarehouseArgs = { id: Scalars["ID"]["input"]; }; -export type MutationDigitalContentCreateArgs = { - input: DigitalContentUploadInput; - variantId: Scalars["ID"]["input"]; -}; - -export type MutationDigitalContentDeleteArgs = { - variantId: Scalars["ID"]["input"]; -}; - -export type MutationDigitalContentUpdateArgs = { - input: DigitalContentInput; - variantId: Scalars["ID"]["input"]; -}; - -export type MutationDigitalContentUrlCreateArgs = { - input: DigitalContentUrlCreateInput; +export type MutationDeliveryOptionsCalculateArgs = { + id: Scalars["ID"]["input"]; }; export type MutationDraftOrderBulkDeleteArgs = { @@ -15711,15 +15498,6 @@ export type NavigationType = /** Secondary storefront navigation. */ | "SECONDARY"; -/** Represents the NEW_TAB target options for an app extension. */ -export type NewTabTargetOptions = { - /** - * HTTP method for New Tab target (GET or POST) - * @deprecated Use `settings` field directly. - */ - method: HttpMethod; -}; - /** An object with an ID */ export type Node = { /** The ID of the object. */ @@ -17347,7 +17125,6 @@ export type OrderLine = Node & * Requires one of the following permissions: MANAGE_PRODUCTS, MANAGE_ORDERS. */ allocations: Maybe>; - digitalContentUrl: Maybe; /** * List of applied discounts * @@ -18498,6 +18275,8 @@ export type PageSortField = | "PUBLICATION_DATE" /** Sort pages by publication date. */ | "PUBLISHED_AT" + /** Sort pages by rank. Note: This option is available only with the `search` filter. */ + | "RANK" /** Sort pages by slug. */ | "SLUG" /** Sort pages by title. */ @@ -18832,7 +18611,7 @@ export type PageTypeUpdateInput = { addAttributes?: InputMaybe>; /** Name of the page type. */ name?: InputMaybe; - /** List of attribute IDs to be assigned to the page type. */ + /** List of attribute IDs to be unassigned from the page type. */ removeAttributes?: InputMaybe>; /** Page type slug. */ slug?: InputMaybe; @@ -18907,6 +18686,18 @@ export type PasswordChange = { user: Maybe; }; +/** + * Controls whether password-based authentication is allowed. + * + * ENABLED - any user can log in with a password. This is the default behavior. + * CUSTOMERS_ONLY - only customer users can log in with a password. + * If a staff user logs in with a password, they will be treated as a customer + * — the issued token will not contain any staff permissions. + * DISABLED - no user can log in with a password. + * + */ +export type PasswordLoginModeEnum = "CUSTOMERS_ONLY" | "DISABLED" | "ENABLED"; + /** Represents a payment of a given type. */ export type Payment = Node & ObjectWithMetadata & { @@ -18964,11 +18755,6 @@ export type Payment = Node & modified: Scalars["DateTime"]["output"]; /** Order associated with a payment. */ order: Maybe; - /** - * Informs whether this is a partial payment. - * @deprecated This field is reserved for the Adyen Gateway plugin. For other gateways, its value is always `false`. This field will be removed in 3.23 along with the plugin. - */ - partial: Scalars["Boolean"]["output"]; /** Type of method used for payment. */ paymentMethodType: Scalars["String"]["output"]; /** List of private metadata items. Requires staff permissions to access. */ @@ -19372,13 +19158,19 @@ export type PaymentMethodDetailsFilterInput = { }; /** - * Details of the payment method used for the transaction. One of `card` or `other` is required. + * Details of the payment method used for the transaction. One of `card`, `other`, or `giftCard` is required. * * Added in Saleor 3.22. */ export type PaymentMethodDetailsInput = { /** Details of the card payment method used for the transaction. */ card?: InputMaybe; + /** + * Details of the gift card payment method used for the transaction. + * + * Added in Saleor 3.23. + */ + giftCard?: InputMaybe; /** Details of the non-card payment method used for this transaction. */ other?: InputMaybe; }; @@ -19524,10 +19316,11 @@ export type PaymentMethodTokenizationResult = * The following types are possible: * CARD - represents a card payment method. * OTHER - represents any payment method that is not a card payment. + * GIFT_CARD - represents a gift card payment method. * * */ -export type PaymentMethodTypeEnum = "CARD" | "OTHER"; +export type PaymentMethodTypeEnum = "CARD" | "GIFT_CARD" | "OTHER"; export type PaymentMethodTypeEnumFilterInput = { /** The value equal to. */ @@ -20785,8 +20578,7 @@ export type ProductErrorCode = | "REQUIRED" | "UNIQUE" | "UNSUPPORTED_MEDIA_PROVIDER" - | "UNSUPPORTED_MIME_TYPE" - | "VARIANT_NO_DIGITAL_CONTENT"; + | "UNSUPPORTED_MIME_TYPE"; /** Event sent when product export is completed. */ export type ProductExportCompleted = Event & { @@ -21373,11 +21165,17 @@ export type ProductType = Node & * Requires one of the following permissions: MANAGE_PRODUCTS. */ availableAttributes: Maybe; - /** Whether the product type has variants. */ + /** + * Whether the product type has variants. + * @deprecated This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. + */ hasVariants: Scalars["Boolean"]["output"]; /** The ID of the product type. */ id: Scalars["ID"]["output"]; - /** Whether the product type is digital. */ + /** + * Whether the product type is digital - doesn't have any effect, it's present for backward-compatibility. + * @deprecated Will be removed in v3.24.0, use metadata or attributes instead. + */ isDigital: Scalars["Boolean"]["output"]; /** Whether shipping is required for this product type. */ isShippingRequired: Scalars["Boolean"]["output"]; @@ -21541,6 +21339,11 @@ export type ProductTypeDelete = { export type ProductTypeEnum = "DIGITAL" | "SHIPPABLE"; export type ProductTypeFilterInput = { + /** + * + * + * DEPRECATED: this field will be removed. The field has no effect on the API behavior. This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. + */ configurable?: InputMaybe; ids?: InputMaybe>; kind?: InputMaybe; @@ -21551,9 +21354,13 @@ export type ProductTypeFilterInput = { }; export type ProductTypeInput = { - /** Determines if product of this type has multiple variants. This option mainly simplifies product management in the dashboard. There is always at least one variant created under the hood. */ + /** + * Determines if product of this type has multiple variants. This option mainly simplifies product management in the dashboard. There is always at least one variant created under the hood. + * + * DEPRECATED: this field will be removed. The field has no effect on the API behavior. This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. + */ hasVariants?: InputMaybe; - /** Determines if products are digital. */ + /** Determines if products are digital - doesn't have any effect, it's present for backward-compatibility. */ isDigital?: InputMaybe; /** Determines if shipping is required for products of this variant. */ isShippingRequired?: InputMaybe; @@ -21685,12 +21492,6 @@ export type ProductVariant = Node & channelListings: Maybe>; /** The date and time when the product variant was created. */ created: Scalars["DateTime"]["output"]; - /** - * Digital content for the product variant. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - */ - digitalContent: Maybe; /** External ID of this product. */ externalReference: Maybe; /** The ID of the product variant. */ @@ -22077,7 +21878,9 @@ export type ProductVariantChannelListing = Node & { /** The price of the variant. */ price: Maybe; /** - * Prior price of the variant used for discount calculations. + * Previous price of the variant in channel. Useful for providing promotion information required by customer protection laws such as EU Omnibus directive. + * + * Warning: This field is not updated automatically. Use Channel Listings mutation to update it manually. * * Added in Saleor 3.21. */ @@ -23699,20 +23502,6 @@ export type Query = { * Requires one of the following permissions: MANAGE_ORDERS, MANAGE_USERS. */ customers: Maybe; - /** - * Look up digital content by ID. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContent: Maybe; - /** - * List of digital content. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContents: Maybe; /** * List of draft orders. The query will not initiate any external requests, including filtering available shipping methods, or performing external tax calculations. * @@ -24039,7 +23828,7 @@ export type Query = { }; export type Query_EntitiesArgs = { - representations?: InputMaybe>>; + representations: Array; }; export type QueryAddressArgs = { @@ -24169,17 +23958,6 @@ export type QueryCustomersArgs = { where?: InputMaybe; }; -export type QueryDigitalContentArgs = { - id: Scalars["ID"]["input"]; -}; - -export type QueryDigitalContentsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - export type QueryDraftOrdersArgs = { after?: InputMaybe; before?: InputMaybe; @@ -24526,6 +24304,7 @@ export type QueryTransactionsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortBy?: InputMaybe; where?: InputMaybe; }; @@ -24658,7 +24437,7 @@ export type RefundSettingsErrorCode = "GRAPHQL_ERROR" | "INVALID" | "REQUIRED"; export type RefundSettingsUpdate = { errors: Array; /** Refund settings. */ - refundSettings: RefundSettings; + refundSettings: Maybe; /** @deprecated Use `errors` field instead. */ refundSettingsErrors: Array; }; @@ -25389,7 +25168,10 @@ export type ShippingMethod = Node & id: Scalars["ID"]["output"]; /** Maximum delivery days for this shipping method. */ maximumDeliveryDays: Maybe; - /** Maximum order price for this shipping method. */ + /** + * Maximum order price for this shipping method. + * @deprecated No longer supported + */ maximumOrderPrice: Maybe; /** * Maximum order weight for this shipping method. @@ -25410,7 +25192,10 @@ export type ShippingMethod = Node & metafields: Maybe; /** Minimum delivery days for this shipping method. */ minimumDeliveryDays: Maybe; - /** Minimal order price for this shipping method. */ + /** + * Minimal order price for this shipping method. + * @deprecated No longer supported + */ minimumOrderPrice: Maybe; /** * Minimum order weight for this shipping method. @@ -26157,12 +25942,6 @@ export type Shop = ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_SETTINGS. */ allowLoginWithoutConfirmation: Maybe; - /** - * Enable automatic fulfillment for all digital products. - * - * Requires one of the following permissions: MANAGE_SETTINGS. - */ - automaticFulfillmentDigitalProducts: Maybe; /** List of available external authentications. */ availableExternalAuthentications: Array; /** List of available payment gateways. */ @@ -26196,18 +25975,6 @@ export type Shop = ObjectWithMetadata & { customerSetPasswordUrl: Maybe; /** Shop's default country. */ defaultCountry: Maybe; - /** - * Default number of max downloads per digital content URL. - * - * Requires one of the following permissions: MANAGE_SETTINGS. - */ - defaultDigitalMaxDownloads: Maybe; - /** - * Default number of days which digital content URL will be valid. - * - * Requires one of the following permissions: MANAGE_SETTINGS. - */ - defaultDigitalUrlValidDays: Maybe; /** * Default shop's email sender's address. * @@ -26277,6 +26044,12 @@ export type Shop = ObjectWithMetadata & { metafields: Maybe; /** Shop's name. */ name: Scalars["String"]["output"]; + /** + * Controls whether password-based authentication is allowed. + * + * Added in Saleor 3.23. + */ + passwordLoginMode: PasswordLoginModeEnum; /** List of available permissions. */ permissions: Array; /** List of possible phone prefixes. */ @@ -26323,6 +26096,12 @@ export type Shop = ObjectWithMetadata & { trackInventoryByDefault: Maybe; /** Returns translated shop fields for the given language code. */ translation: Maybe; + /** + * When enabled, stock availability is filtered by shipping zones and the destination address (legacy behavior). When disabled, stock availability is determined only by the direct warehouse-channel link, ignoring shipping zones. + * + * Added in Saleor 3.23. + */ + useLegacyShippingZoneStockAvailability: Scalars["Boolean"]["output"]; /** * Use legacy update webhook emission. When enabled, update webhooks (e.g. `customerUpdated`,`productVariantUpdated`) are sent even when only metadata changes. When disabled, update webhooks are not sent for metadata-only changes; only metadata-specific webhooks (e.g., `customerMetadataUpdated`, `productVariantMetadataUpdated`) are sent. * @@ -26422,6 +26201,7 @@ export type ShopErrorCode = | "GRAPHQL_ERROR" | "INVALID" | "NOT_FOUND" + | "PASSWORD_AUTH_RESTRICTION" | "REQUIRED" | "UNIQUE"; @@ -26455,8 +26235,6 @@ export type ShopMetadataUpdated = Event & { export type ShopSettingsInput = { /** Enable possibility to login without account confirmation. */ allowLoginWithoutConfirmation?: InputMaybe; - /** Enable automatic fulfillment for all digital products. */ - automaticFulfillmentDigitalProducts?: InputMaybe; /** * Charge taxes on shipping. * @@ -26465,10 +26243,6 @@ export type ShopSettingsInput = { chargeTaxesOnShipping?: InputMaybe; /** URL of a view where customers can set their password. */ customerSetPasswordUrl?: InputMaybe; - /** Default number of max downloads per digital content URL. */ - defaultDigitalMaxDownloads?: InputMaybe; - /** Default number of days which digital content URL will be valid. */ - defaultDigitalUrlValidDays?: InputMaybe; /** Default email sender's address. */ defaultMailSenderAddress?: InputMaybe; /** Default email sender's name. */ @@ -26505,6 +26279,12 @@ export type ShopSettingsInput = { * Warning: never store sensitive information, including financial data such as credit card details. */ metadata?: InputMaybe>; + /** + * Controls whether password-based authentication is allowed. + * + * Added in Saleor 3.23. + */ + passwordLoginMode?: InputMaybe; /** * When enabled, address fields that are not valid for a given country (according to Google's i18n address data) will be preserved instead of being removed during validation. Validation errors are still returned. * @@ -26523,6 +26303,14 @@ export type ShopSettingsInput = { reserveStockDurationAuthenticatedUser?: InputMaybe; /** This field is used as a default value for `ProductVariant.trackInventory`. */ trackInventoryByDefault?: InputMaybe; + /** + * When enabled, stock availability is filtered by shipping zones and the destination address (legacy behavior). When disabled, stock availability is determined only by the direct warehouse-channel link, ignoring shipping zones. + * + * Added in Saleor 3.23. + */ + useLegacyShippingZoneStockAvailability?: InputMaybe< + Scalars["Boolean"]["input"] + >; /** * Use legacy update webhook emission. When enabled, update webhooks (e.g. `customerUpdated`,`productVariantUpdated`) are sent even when only metadata changes. When disabled, update webhooks are not sent for metadata-only changes; only metadata-specific webhooks (e.g., `customerMetadataUpdated`, `productVariantMetadataUpdated`) are sent. * @@ -28154,6 +27942,26 @@ export type TransactionEvent = Node & { type: Maybe; }; +/** + * Filter input for transaction events data. + * + * Added in Saleor 3.23. + */ +export type TransactionEventFilterInput = { + /** + * Filter transaction events by created at date. + * + * Added in Saleor 3.23. + */ + createdAt?: InputMaybe; + /** + * Filter transaction events by type. + * + * Added in Saleor 3.23. + */ + type?: InputMaybe; +}; + export type TransactionEventInput = { /** The message related to the event. */ message?: InputMaybe; @@ -28246,6 +28054,13 @@ export type TransactionEventTypeEnum = | "REFUND_REVERSE" | "REFUND_SUCCESS"; +export type TransactionEventTypeEnumFilterInput = { + /** The value equal to. */ + eq?: InputMaybe; + /** The value included in. */ + oneOf?: InputMaybe>; +}; + /** Filter input for transactions. */ export type TransactionFilterInput = { /** Filter by metadata fields of transactions. */ @@ -28590,6 +28405,27 @@ export type TransactionRequestRefundForGrantedRefundErrorCode = | "REFUND_ALREADY_PROCESSED" | "REFUND_IS_PENDING"; +export type TransactionSortField = + /** + * Sort transactions by creation date. + * + * Added in Saleor 3.23. + */ + | "CREATED_AT" + /** + * Sort transactions by modification date. + * + * Added in Saleor 3.23. + */ + | "MODIFIED_AT"; + +export type TransactionSortingInput = { + /** Specifies the direction in which to sort transactions. */ + direction: OrderDirection; + /** Sort transactions by the selected field. */ + field: TransactionSortField; +}; + /** * Update transaction. * @@ -28663,7 +28499,25 @@ export type TransactionWhereInput = { OR?: InputMaybe>; /** Filter by app identifier. */ appIdentifier?: InputMaybe; + /** + * Filter transactions by created at date. + * + * Added in Saleor 3.23. + */ + createdAt?: InputMaybe; + /** + * Filter by transaction events. Each list item represents conditions that must be satisfied by a single event. The filter matches transactions that have related events meeting all specified groups of conditions. + * + * Added in Saleor 3.23. + */ + events?: InputMaybe>; ids?: InputMaybe>; + /** + * Filter transactions by modified at date. + * + * Added in Saleor 3.23. + */ + modifiedAt?: InputMaybe; /** Filter by PSP reference. */ pspReference?: InputMaybe; }; @@ -29151,7 +29005,9 @@ export type UserSortField = /** Sort users by last name. */ | "LAST_NAME" /** Sort users by order count. */ - | "ORDER_COUNT"; + | "ORDER_COUNT" + /** Sort users by rank. Note: This option is available only with the `search` filter. */ + | "RANK"; export type UserSortingInput = { /** Specifies the direction in which to sort users. */ @@ -31332,15 +31188,6 @@ export type Weight = { export type WeightUnitsEnum = "G" | "KG" | "LB" | "OZ" | "TONNE"; -/** Represents the WIDGET target options for an app extension. */ -export type WidgetTargetOptions = { - /** - * HTTP method for Widget target (GET or POST) - * @deprecated Use `settings` field directly. - */ - method: HttpMethod; -}; - /** _Entity union as defined by Federation spec. */ export type _Entity = | Address diff --git a/packages/codegen/schema.ts b/packages/codegen/schema.ts index 94378d8b..a03bf50c 100644 --- a/packages/codegen/schema.ts +++ b/packages/codegen/schema.ts @@ -252,6 +252,7 @@ export type AccountErrorCode = | 'DELETE_OWN_ACCOUNT' | 'DELETE_STAFF_ACCOUNT' | 'DELETE_SUPERUSER_ACCOUNT' + | 'DISABLED_AUTHENTICATION_METHOD' | 'DUPLICATED_INPUT_ITEM' | 'GRAPHQL_ERROR' | 'INACTIVE' @@ -1042,38 +1043,21 @@ export type AppExtension = Node & { /** Label of the extension to show in the dashboard. */ label: Scalars['String']['output']; /** - * Place where given extension will be mounted. - * @deprecated Use `mountName` instead. - */ - mount: AppExtensionMountEnum; - /** - * Name of the extension mount point in the dashboard. Replaces `mount` + * Name of the extension mount point in the dashboard. Value returned in UPPERCASE. * * Added in Saleor 3.22. */ mountName: Scalars['String']['output']; - /** - * App extension options. - * - * Added in Saleor 3.22. - * @deprecated Use `settings` field instead. - */ - options: Maybe; /** List of the app extension's permissions. */ permissions: Array; /** - * App extension settings. Replaces `options` field. + * App extension settings. * * Added in Saleor 3.22. */ settings: Scalars['JSON']['output']; /** - * Type of way how app extension will be opened. - * @deprecated Use `targetName` instead. - */ - target: AppExtensionTargetEnum; - /** - * Name of the extension target in the dashboard. Replaces `target` + * Name of the extension target in the dashboard. Value returned in UPPERCASE. * * Added in Saleor 3.22. */ @@ -1098,24 +1082,12 @@ export type AppExtensionCountableEdge = { }; export type AppExtensionFilterInput = { - /** - * DEPRECATED: Use `mountName` instead. - * - * DEPRECATED: this field will be removed. - */ - mount?: InputMaybe>; /** * Plain-text mount name (case insensitive) * * Added in Saleor 3.22. */ mountName?: InputMaybe>; - /** - * DEPRECATED: Use `targetName` instead. - * - * DEPRECATED: this field will be removed. - */ - target?: InputMaybe; /** * Plain-text target name (case insensitive) * @@ -1124,92 +1096,6 @@ export type AppExtensionFilterInput = { targetName?: InputMaybe; }; -/** All places where app extension can be mounted. */ -export type AppExtensionMountEnum = - | 'CATEGORY_DETAILS_MORE_ACTIONS' - | 'CATEGORY_OVERVIEW_CREATE' - | 'CATEGORY_OVERVIEW_MORE_ACTIONS' - | 'COLLECTION_DETAILS_MORE_ACTIONS' - | 'COLLECTION_DETAILS_WIDGETS' - | 'COLLECTION_OVERVIEW_CREATE' - | 'COLLECTION_OVERVIEW_MORE_ACTIONS' - | 'CUSTOMER_DETAILS_MORE_ACTIONS' - | 'CUSTOMER_DETAILS_WIDGETS' - | 'CUSTOMER_OVERVIEW_CREATE' - | 'CUSTOMER_OVERVIEW_MORE_ACTIONS' - | 'DISCOUNT_DETAILS_MORE_ACTIONS' - | 'DISCOUNT_OVERVIEW_CREATE' - | 'DISCOUNT_OVERVIEW_MORE_ACTIONS' - | 'DRAFT_ORDER_DETAILS_MORE_ACTIONS' - | 'DRAFT_ORDER_DETAILS_WIDGETS' - | 'DRAFT_ORDER_OVERVIEW_CREATE' - | 'DRAFT_ORDER_OVERVIEW_MORE_ACTIONS' - | 'GIFT_CARD_DETAILS_MORE_ACTIONS' - | 'GIFT_CARD_DETAILS_WIDGETS' - | 'GIFT_CARD_OVERVIEW_CREATE' - | 'GIFT_CARD_OVERVIEW_MORE_ACTIONS' - | 'MENU_DETAILS_MORE_ACTIONS' - | 'MENU_OVERVIEW_CREATE' - | 'MENU_OVERVIEW_MORE_ACTIONS' - | 'NAVIGATION_CATALOG' - | 'NAVIGATION_CUSTOMERS' - | 'NAVIGATION_DISCOUNTS' - | 'NAVIGATION_ORDERS' - | 'NAVIGATION_PAGES' - | 'NAVIGATION_TRANSLATIONS' - | 'ORDER_DETAILS_MORE_ACTIONS' - | 'ORDER_DETAILS_WIDGETS' - | 'ORDER_OVERVIEW_CREATE' - | 'ORDER_OVERVIEW_MORE_ACTIONS' - | 'PAGE_DETAILS_MORE_ACTIONS' - | 'PAGE_OVERVIEW_CREATE' - | 'PAGE_OVERVIEW_MORE_ACTIONS' - | 'PAGE_TYPE_DETAILS_MORE_ACTIONS' - | 'PAGE_TYPE_OVERVIEW_CREATE' - | 'PAGE_TYPE_OVERVIEW_MORE_ACTIONS' - | 'PRODUCT_DETAILS_MORE_ACTIONS' - | 'PRODUCT_DETAILS_WIDGETS' - | 'PRODUCT_OVERVIEW_CREATE' - | 'PRODUCT_OVERVIEW_MORE_ACTIONS' - | 'TRANSLATIONS_MORE_ACTIONS' - | 'VOUCHER_DETAILS_MORE_ACTIONS' - | 'VOUCHER_DETAILS_WIDGETS' - | 'VOUCHER_OVERVIEW_CREATE' - | 'VOUCHER_OVERVIEW_MORE_ACTIONS'; - -/** Represents the options for an app extension. */ -export type AppExtensionOptionsNewTab = { - /** - * Options controlling behavior of the NEW_TAB extension target - * @deprecated Use `settings` field directly. - */ - newTabTarget: Maybe; -}; - -/** Represents the options for an app extension. */ -export type AppExtensionOptionsWidget = { - /** - * Options for displaying a Widget - * @deprecated Use `settings` field directly. - */ - widgetTarget: Maybe; -}; - -export type AppExtensionPossibleOptions = AppExtensionOptionsNewTab | AppExtensionOptionsWidget; - -/** - * All available ways of opening an app extension. - * - * POPUP - app's extension will be mounted as a popup window - * APP_PAGE - redirect to app's page - * - */ -export type AppExtensionTargetEnum = - | 'APP_PAGE' - | 'NEW_TAB' - | 'POPUP' - | 'WIDGET'; - /** * Fetch and validate manifest. * @@ -1254,9 +1140,9 @@ export type AppInstallInput = { /** Determine if app will be set active or not. */ activateAfterInstallation?: InputMaybe; /** Name of the app to install. */ - appName?: InputMaybe; + appName: Scalars['String']['input']; /** URL to app's manifest in JSON format. */ - manifestUrl?: InputMaybe; + manifestUrl: Scalars['String']['input']; /** List of permission code names to assign to this app. */ permissions?: InputMaybe>; }; @@ -1318,12 +1204,7 @@ export type AppManifestExtension = { /** Label of the extension to show in the dashboard. */ label: Scalars['String']['output']; /** - * Place where given extension will be mounted. - * @deprecated Use `mountName` instead. - */ - mount: AppExtensionMountEnum; - /** - * Name of the extension mount point in the dashboard. Replaces `mount` + * Name of the extension mount point in the dashboard. Value returned in UPPERCASE. * * Added in Saleor 3.22. */ @@ -1331,18 +1212,13 @@ export type AppManifestExtension = { /** List of the app extension's permissions. */ permissions: Array; /** - * JSON object with settings for this extension. + * App extension settings. * * Added in Saleor 3.22. */ settings: Scalars['JSON']['output']; /** - * Type of way how app extension will be opened. - * @deprecated Use `targetName` instead. - */ - target: AppExtensionTargetEnum; - /** - * Name of the extension target in the dashboard. Replaces `target` + * Name of the extension target in the dashboard. Value returned in UPPERCASE. * * Added in Saleor 3.22. */ @@ -2185,7 +2061,7 @@ export type Attribute = Node & ObjectWithMetadata & { /** Public metadata. Use `keys` to control which fields you want to include. The default is to include everything. */ metafields: Maybe; /** Name of an attribute displayed in the interface. */ - name: Maybe; + name: Scalars['String']['output']; /** List of private metadata items. Requires staff permissions to access. */ privateMetadata: Array; /** @@ -2207,7 +2083,7 @@ export type Attribute = Node & ObjectWithMetadata & { */ referenceTypes: Maybe>; /** Internal representation of an attribute name. */ - slug: Maybe; + slug: Scalars['String']['output']; /** * The position of the attribute in the storefront navigation (0 by default). Requires one of the following permissions: MANAGE_PAGES, MANAGE_PAGE_TYPES_AND_ATTRIBUTES, MANAGE_PRODUCTS, MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES. * @deprecated No longer supported @@ -2216,7 +2092,7 @@ export type Attribute = Node & ObjectWithMetadata & { /** Returns translated attribute fields for the given language code. */ translation: Maybe; /** The attribute type. */ - type: Maybe; + type: AttributeTypeEnum; /** The unit of attribute values. */ unit: Maybe; /** Whether the attribute requires values to be passed or not. Requires one of the following permissions: MANAGE_PAGES, MANAGE_PAGE_TYPES_AND_ATTRIBUTES, MANAGE_PRODUCTS, MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES. */ @@ -4351,12 +4227,19 @@ export type Checkout = Node & ObjectWithMetadata & { * Added in Saleor 3.21. */ customerNote: Scalars['String']['output']; + /** + * The delivery method selected for this checkout. + * + * Added in Saleor 3.23. + */ + delivery: Maybe; /** * The delivery method selected for this checkout. * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Optionally triggered when cached external shipping methods are invalid. * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. + * @deprecated Use `delivery` instead. */ deliveryMethod: Maybe; /** The total discount applied to the checkout. Note: Only discount created via voucher are included in this field. */ @@ -4416,7 +4299,7 @@ export type Checkout = Node & ObjectWithMetadata & { * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Optionally triggered when cached external shipping methods are invalid. * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. - * @deprecated Use `deliveryMethod` instead. + * @deprecated Use `delivery` instead. */ shippingMethod: Maybe; /** @@ -4822,6 +4705,7 @@ export type CheckoutCustomerNoteUpdate = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ export type CheckoutDeliveryMethodUpdate = { @@ -5220,7 +5104,25 @@ export type CheckoutPaymentCreate = { }; /** Represents an problem in the checkout. */ -export type CheckoutProblem = CheckoutLineProblemInsufficientStock | CheckoutLineProblemVariantNotAvailable; +export type CheckoutProblem = CheckoutLineProblemInsufficientStock | CheckoutLineProblemVariantNotAvailable | CheckoutProblemDeliveryMethodInvalid | CheckoutProblemDeliveryMethodStale; + +/** + * Indicates that the selected delivery method is invalid. + * + * Added in Saleor 3.23. + */ +export type CheckoutProblemDeliveryMethodInvalid = { + delivery: Delivery; +}; + +/** + * Indicates that the delivery methods are stale. + * + * Added in Saleor 3.23. + */ +export type CheckoutProblemDeliveryMethodStale = { + delivery: Delivery; +}; /** * Remove a gift card or a voucher from a checkout. @@ -5238,6 +5140,12 @@ export type CheckoutRemovePromoCode = { /** Represents the channel-specific checkout settings. */ export type CheckoutSettings = { + /** + * Default to `true`. Determines whether gift cards can be attached to a Checkout via `addPromoCode` mutation. Usage of this mutation with gift cards is deprecated. + * + * Added in Saleor 3.23. + */ + allowLegacyGiftCardUse: Scalars['Boolean']['output']; /** * The date time defines the earliest checkout creation date on which fully paid checkouts can begin to be automatically completed. * @@ -5265,6 +5173,12 @@ export type CheckoutSettings = { }; export type CheckoutSettingsInput = { + /** + * Default to `true`. Determines whether gift cards can be attached to a Checkout via `addPromoCode` mutation. Usage of this mutation with gift cards is deprecated. + * + * Added in Saleor 3.23. + */ + allowLegacyGiftCardUse?: InputMaybe; /** * Settings for automatic completion of fully paid checkouts. * @@ -5306,6 +5220,7 @@ export type CheckoutShippingAddressUpdate = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout shipping method with the external one. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ export type CheckoutShippingMethodUpdate = { @@ -5322,7 +5237,9 @@ export type CheckoutSortField = /** Sort checkouts by customer. */ | 'CUSTOMER' /** Sort checkouts by payment. */ - | 'PAYMENT'; + | 'PAYMENT' + /** Sort checkouts by rank. Note: This option is available only with the `search` filter. */ + | 'RANK'; export type CheckoutSortingInput = { /** Specifies the direction in which to sort checkouts. */ @@ -6746,8 +6663,6 @@ export type CustomerEvent = Node & { message: Maybe; /** The concerned order. */ order: Maybe; - /** The concerned order line. */ - orderLine: Maybe; /** Customer event type. */ type: Maybe; /** User who performed the action. */ @@ -7013,208 +6928,50 @@ export type DeletePrivateMetadata = { metadataErrors: Array; }; -/** Represents a delivery method chosen for the checkout. `Warehouse` type is used when checkout is marked as "click and collect" and `ShippingMethod` otherwise. */ -export type DeliveryMethod = ShippingMethod | Warehouse; - -/** Represents digital content associated with a product variant. */ -export type DigitalContent = Node & ObjectWithMetadata & { - /** Indicator for automatic fulfillment of digital content. */ - automaticFulfillment: Scalars['Boolean']['output']; - /** File associated with digital content. */ - contentFile: Scalars['String']['output']; - /** The ID of the digital content. */ - id: Scalars['ID']['output']; - /** Maximum number of allowed downloads for the digital content. */ - maxDownloads: Maybe; - /** List of public metadata items. Can be accessed without permissions. */ - metadata: Array; - /** - * A single key from public metadata. - * - * Tip: Use GraphQL aliases to fetch multiple keys. - */ - metafield: Maybe; - /** Public metadata. Use `keys` to control which fields you want to include. The default is to include everything. */ - metafields: Maybe; - /** List of private metadata items. Requires staff permissions to access. */ - privateMetadata: Array; - /** - * A single key from private metadata. Requires staff permissions to access. - * - * Tip: Use GraphQL aliases to fetch multiple keys. - */ - privateMetafield: Maybe; - /** Private metadata. Requires staff permissions to access. Use `keys` to control which fields you want to include. The default is to include everything. */ - privateMetafields: Maybe; - /** Product variant assigned to digital content. */ - productVariant: ProductVariant; - /** Number of days the URL for the digital content remains valid. */ - urlValidDays: Maybe; - /** List of URLs for the digital variant. */ - urls: Maybe>; - /** Default settings indicator for digital content. */ - useDefaultSettings: Scalars['Boolean']['output']; -}; - - -/** Represents digital content associated with a product variant. */ -export type DigitalContentMetafieldArgs = { - key: Scalars['String']['input']; -}; - - -/** Represents digital content associated with a product variant. */ -export type DigitalContentMetafieldsArgs = { - keys?: InputMaybe>; -}; - - -/** Represents digital content associated with a product variant. */ -export type DigitalContentPrivateMetafieldArgs = { - key: Scalars['String']['input']; -}; - - -/** Represents digital content associated with a product variant. */ -export type DigitalContentPrivateMetafieldsArgs = { - keys?: InputMaybe>; -}; - -/** A connection to a list of digital content items. */ -export type DigitalContentCountableConnection = { - edges: Array; - /** Pagination data for this connection. */ - pageInfo: PageInfo; - /** A total count of items in the collection. */ - totalCount: Maybe; -}; - -export type DigitalContentCountableEdge = { - /** A cursor for use in pagination. */ - cursor: Scalars['String']['output']; - /** The item at the end of the edge. */ - node: DigitalContent; -}; - /** - * Create new digital content. This mutation must be sent as a `multipart` request. More detailed specs of the upload format can be found here: https://github.com/jaydenseric/graphql-multipart-request-spec + * Represents a delivery option for the checkout. * - * Requires one of the following permissions: MANAGE_PRODUCTS. - */ -export type DigitalContentCreate = { - content: Maybe; - errors: Array; - /** @deprecated Use `errors` field instead. */ - productErrors: Array; - variant: Maybe; -}; - -/** - * Remove digital content assigned to given variant. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. + * Added in Saleor 3.23. */ -export type DigitalContentDelete = { - errors: Array; - /** @deprecated Use `errors` field instead. */ - productErrors: Array; - variant: Maybe; +export type Delivery = { + /** The ID of the delivery. */ + id: Scalars['ID']['output']; + /** Shipping method represented by the delivery. */ + shippingMethod: Maybe; }; -export type DigitalContentInput = { - /** Overwrite default automatic_fulfillment setting for variant. */ - automaticFulfillment?: InputMaybe; - /** Determines how many times a download link can be accessed by a customer. */ - maxDownloads?: InputMaybe; - /** - * Fields required to update the digital content metadata. Can be read by any API client authorized to read the object it's attached to. - * - * Warning: never store sensitive information, including financial data such as credit card details. - */ - metadata?: InputMaybe>; - /** - * Fields required to update the digital content private metadata. Requires permissions to modify and to read the metadata of the object it's attached to. - * - * Warning: never store sensitive information, including financial data such as credit card details. - */ - privateMetadata?: InputMaybe>; - /** Determines for how many days a download link is active since it was generated. */ - urlValidDays?: InputMaybe; - /** Use default digital content settings for this product. */ - useDefaultSettings: Scalars['Boolean']['input']; -}; +/** Represents a delivery method chosen for the checkout. `Warehouse` type is used when checkout is marked as "click and collect" and `ShippingMethod` otherwise. */ +export type DeliveryMethod = ShippingMethod | Warehouse; /** - * Updates digital content. + * Calculates available delivery options for a checkout. * - * Requires one of the following permissions: MANAGE_PRODUCTS. - */ -export type DigitalContentUpdate = { - content: Maybe; - errors: Array; - /** @deprecated Use `errors` field instead. */ - productErrors: Array; - variant: Maybe; -}; - -export type DigitalContentUploadInput = { - /** Overwrite default automatic_fulfillment setting for variant. */ - automaticFulfillment?: InputMaybe; - /** Represents an file in a multipart request. */ - contentFile: Scalars['Upload']['input']; - /** Determines how many times a download link can be accessed by a customer. */ - maxDownloads?: InputMaybe; - /** - * Fields required to update the digital content metadata. Can be read by any API client authorized to read the object it's attached to. - * - * Warning: never store sensitive information, including financial data such as credit card details. - */ - metadata?: InputMaybe>; - /** - * Fields required to update the digital content private metadata. Requires permissions to modify and to read the metadata of the object it's attached to. - * - * Warning: never store sensitive information, including financial data such as credit card details. - */ - privateMetadata?: InputMaybe>; - /** Determines for how many days a download link is active since it was generated. */ - urlValidDays?: InputMaybe; - /** Use default digital content settings for this product. */ - useDefaultSettings: Scalars['Boolean']['input']; -}; - -/** Represents a URL for digital content. */ -export type DigitalContentUrl = Node & { - /** Digital content associated with the URL. */ - content: DigitalContent; - /** Date and time when the digital content URL was created. */ - created: Scalars['DateTime']['output']; - /** Number of times digital content has been downloaded. */ - downloadNum: Scalars['Int']['output']; - /** The ID of the digital content URL. */ - id: Scalars['ID']['output']; - /** UUID of digital content. */ - token: Scalars['UUID']['output']; - /** URL for digital content. */ - url: Maybe; -}; - -/** - * Generate new URL to digital content. + * Added in Saleor 3.23. * - * Requires one of the following permissions: MANAGE_PRODUCTS. + * Triggers the following webhook events: + * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered to fetch external shipping methods. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Triggered to filter shipping methods. */ -export type DigitalContentUrlCreate = { - digitalContentUrl: Maybe; - errors: Array; - /** @deprecated Use `errors` field instead. */ - productErrors: Array; +export type DeliveryOptionsCalculate = { + /** List of the available deliveries. */ + deliveries: Array; + errors: Array; }; -export type DigitalContentUrlCreateInput = { - /** Digital content ID which URL will belong to. */ - content: Scalars['ID']['input']; +export type DeliveryOptionsCalculateError = { + /** The error code. */ + code: DeliveryOptionsCalculateErrorCode; + /** Name of a field that caused the error. A value of `null` indicates that the error isn't associated with a particular field. */ + field: Maybe; + /** The error message. */ + message: Maybe; }; +export type DeliveryOptionsCalculateErrorCode = + | 'GRAPHQL_ERROR' + | 'INVALID' + | 'NOT_FOUND'; + export type DiscountError = { /** List of channels IDs which causes the error. */ channels: Maybe>; @@ -7384,7 +7141,11 @@ export type DraftOrderCreateInput = { user?: InputMaybe; /** Email address of the customer. */ userEmail?: InputMaybe; - /** ID of the voucher associated with the order. */ + /** + * ID of the voucher associated with the order. + * + * DEPRECATED: this field will be removed. Use `voucherCode` instead. + */ voucher?: InputMaybe; /** * A code of the voucher associated with the order. @@ -7493,7 +7254,11 @@ export type DraftOrderInput = { user?: InputMaybe; /** Email address of the customer. */ userEmail?: InputMaybe; - /** ID of the voucher associated with the order. */ + /** + * ID of the voucher associated with the order. + * + * DEPRECATED: this field will be removed. Use `voucherCode` instead. + */ voucher?: InputMaybe; /** * A code of the voucher associated with the order. @@ -7907,8 +7672,6 @@ export type ExportScope = * * Added in Saleor 3.18. * - * Note: this API is currently in Feature Preview and can be subject to changes at later point. - * * Requires one of the following permissions: MANAGE_DISCOUNTS. * * Triggers the following webhook events: @@ -8389,7 +8152,7 @@ export type GiftCard = Node & ObjectWithMetadata & { */ endDate: Maybe; /** - * List of events associated with the gift card. Requires MANAGE_GIFT_CARD permission to access all events. Users with MANAGE_ORDERS permission can access only USED_IN_ORDER events. + * List of events associated with the gift card. Requires MANAGE_GIFT_CARD permission to access all events. Users with MANAGE_ORDERS permission can access only USED_IN_ORDER and REFUNDED_IN_ORDER events. * * Requires one of the following permissions: MANAGE_GIFT_CARD, MANAGE_ORDERS. */ @@ -8811,6 +8574,7 @@ export type GiftCardEventsEnum = | 'EXPIRY_DATE_UPDATED' | 'ISSUED' | 'NOTE_ADDED' + | 'REFUNDED_IN_ORDER' | 'RESENT' | 'SENT_TO_CUSTOMER' | 'TAGS_UPDATED' @@ -8859,6 +8623,55 @@ export type GiftCardMetadataUpdated = Event & { version: Maybe; }; +/** + * Represents a gift card payment method used for a transaction. + * + * Added in Saleor 3.23. + */ +export type GiftCardPaymentMethodDetails = PaymentMethodDetails & { + /** + * Brand of the gift card. + * + * Added in Saleor 3.23. + */ + brand: Maybe; + /** + * Indicates whether the gift card is a built-in Saleor gift card. + * + * Added in Saleor 3.23. + */ + isSaleorGiftcard: Scalars['Boolean']['output']; + /** + * Last characters of the gift card code. Max 4 characters. + * + * Added in Saleor 3.23. + */ + lastChars: Maybe; + /** Name of the gift card. */ + name: Scalars['String']['output']; +}; + +export type GiftCardPaymentMethodDetailsInput = { + /** + * Brand of the gift card used for the transaction. Max length is 40 characters. + * + * Added in Saleor 3.23. + */ + brand?: InputMaybe; + /** + * Last characters of the gift card used for the transaction. Max length is 4 characters. + * + * Added in Saleor 3.23. + */ + lastChars?: InputMaybe; + /** + * Name of the payment method used for the transaction. Max length is 256 characters. + * + * Added in Saleor 3.23. + */ + name: Scalars['String']['input']; +}; + /** * Resend a gift card. * @@ -8951,6 +8764,8 @@ export type GiftCardSortField = | 'CURRENT_BALANCE' /** Sort gift cards by product. */ | 'PRODUCT' + /** Sort gift cards by rank. Note: This option is available only with the `search` filter. */ + | 'RANK' /** Sort gift cards by used by. */ | 'USED_BY'; @@ -9115,10 +8930,6 @@ export type GroupCountableEdge = { node: Group; }; -export type HttpMethod = - | 'GET' - | 'POST'; - /** Thumbnail formats for icon images. */ export type IconThumbnailFormatEnum = | 'ORIGINAL' @@ -12342,6 +12153,7 @@ export type Mutation = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ checkoutDeliveryMethodUpdate: Maybe; @@ -12409,6 +12221,7 @@ export type Mutation = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout shipping method with the external one. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. * @deprecated Use `checkoutDeliveryMethodUpdate` instead. */ @@ -12552,33 +12365,15 @@ export type Mutation = { */ deleteWarehouse: Maybe; /** - * Create new digital content. This mutation must be sent as a `multipart` request. More detailed specs of the upload format can be found here: https://github.com/jaydenseric/graphql-multipart-request-spec + * Calculates available delivery options for a checkout. * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContentCreate: Maybe; - /** - * Remove digital content assigned to given variant. + * Added in Saleor 3.23. * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContentDelete: Maybe; - /** - * Updates digital content. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContentUpdate: Maybe; - /** - * Generate new URL to digital content. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + * Triggers the following webhook events: + * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered to fetch external shipping methods. + * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Triggered to filter shipping methods. */ - digitalContentUrlCreate: Maybe; + deliveryOptionsCalculate: Maybe; /** * Deletes draft orders. * @@ -12630,6 +12425,7 @@ export type Mutation = { * Triggers the following webhook events: * - NOTIFY_USER (async): A notification for the exported file. * - GIFT_CARD_EXPORT_COMPLETED (async): A notification for the exported file. + * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportGiftCards: Maybe; /** @@ -12640,6 +12436,7 @@ export type Mutation = { * Triggers the following webhook events: * - NOTIFY_USER (async): A notification for the exported file. * - PRODUCT_EXPORT_COMPLETED (async): A notification for the exported file. + * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportProducts: Maybe; /** @@ -12647,12 +12444,11 @@ export type Mutation = { * * Added in Saleor 3.18. * - * Note: this API is currently in Feature Preview and can be subject to changes at later point. - * * Requires one of the following permissions: MANAGE_DISCOUNTS. * * Triggers the following webhook events: * - VOUCHER_CODE_EXPORT_COMPLETED (async): A notification for the exported file. + * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportVoucherCodes: Maybe; /** Prepare external authentication URL for user by custom plugin. */ @@ -14754,25 +14550,8 @@ export type MutationDeleteWarehouseArgs = { }; -export type MutationDigitalContentCreateArgs = { - input: DigitalContentUploadInput; - variantId: Scalars['ID']['input']; -}; - - -export type MutationDigitalContentDeleteArgs = { - variantId: Scalars['ID']['input']; -}; - - -export type MutationDigitalContentUpdateArgs = { - input: DigitalContentInput; - variantId: Scalars['ID']['input']; -}; - - -export type MutationDigitalContentUrlCreateArgs = { - input: DigitalContentUrlCreateInput; +export type MutationDeliveryOptionsCalculateArgs = { + id: Scalars['ID']['input']; }; @@ -16129,15 +15908,6 @@ export type NavigationType = /** Secondary storefront navigation. */ | 'SECONDARY'; -/** Represents the NEW_TAB target options for an app extension. */ -export type NewTabTargetOptions = { - /** - * HTTP method for New Tab target (GET or POST) - * @deprecated Use `settings` field directly. - */ - method: HttpMethod; -}; - /** An object with an ID */ export type Node = { /** The ID of the object. */ @@ -17780,7 +17550,6 @@ export type OrderLine = Node & ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_PRODUCTS, MANAGE_ORDERS. */ allocations: Maybe>; - digitalContentUrl: Maybe; /** * List of applied discounts * @@ -18945,6 +18714,8 @@ export type PageSortField = | 'PUBLICATION_DATE' /** Sort pages by publication date. */ | 'PUBLISHED_AT' + /** Sort pages by rank. Note: This option is available only with the `search` filter. */ + | 'RANK' /** Sort pages by slug. */ | 'SLUG' /** Sort pages by title. */ @@ -19284,7 +19055,7 @@ export type PageTypeUpdateInput = { addAttributes?: InputMaybe>; /** Name of the page type. */ name?: InputMaybe; - /** List of attribute IDs to be assigned to the page type. */ + /** List of attribute IDs to be unassigned from the page type. */ removeAttributes?: InputMaybe>; /** Page type slug. */ slug?: InputMaybe; @@ -19359,6 +19130,21 @@ export type PasswordChange = { user: Maybe; }; +/** + * Controls whether password-based authentication is allowed. + * + * ENABLED - any user can log in with a password. This is the default behavior. + * CUSTOMERS_ONLY - only customer users can log in with a password. + * If a staff user logs in with a password, they will be treated as a customer + * — the issued token will not contain any staff permissions. + * DISABLED - no user can log in with a password. + * + */ +export type PasswordLoginModeEnum = + | 'CUSTOMERS_ONLY' + | 'DISABLED' + | 'ENABLED'; + /** Represents a payment of a given type. */ export type Payment = Node & ObjectWithMetadata & { /** @@ -19415,11 +19201,6 @@ export type Payment = Node & ObjectWithMetadata & { modified: Scalars['DateTime']['output']; /** Order associated with a payment. */ order: Maybe; - /** - * Informs whether this is a partial payment. - * @deprecated This field is reserved for the Adyen Gateway plugin. For other gateways, its value is always `false`. This field will be removed in 3.23 along with the plugin. - */ - partial: Scalars['Boolean']['output']; /** Type of method used for payment. */ paymentMethodType: Scalars['String']['output']; /** List of private metadata items. Requires staff permissions to access. */ @@ -19827,13 +19608,19 @@ export type PaymentMethodDetailsFilterInput = { }; /** - * Details of the payment method used for the transaction. One of `card` or `other` is required. + * Details of the payment method used for the transaction. One of `card`, `other`, or `giftCard` is required. * * Added in Saleor 3.22. */ export type PaymentMethodDetailsInput = { /** Details of the card payment method used for the transaction. */ card?: InputMaybe; + /** + * Details of the gift card payment method used for the transaction. + * + * Added in Saleor 3.23. + */ + giftCard?: InputMaybe; /** Details of the non-card payment method used for this transaction. */ other?: InputMaybe; }; @@ -19979,11 +19766,13 @@ export type PaymentMethodTokenizationResult = * The following types are possible: * CARD - represents a card payment method. * OTHER - represents any payment method that is not a card payment. + * GIFT_CARD - represents a gift card payment method. * * */ export type PaymentMethodTypeEnum = | 'CARD' + | 'GIFT_CARD' | 'OTHER'; export type PaymentMethodTypeEnumFilterInput = { @@ -21267,8 +21056,7 @@ export type ProductErrorCode = | 'REQUIRED' | 'UNIQUE' | 'UNSUPPORTED_MEDIA_PROVIDER' - | 'UNSUPPORTED_MIME_TYPE' - | 'VARIANT_NO_DIGITAL_CONTENT'; + | 'UNSUPPORTED_MIME_TYPE'; /** Event sent when product export is completed. */ export type ProductExportCompleted = Event & { @@ -21863,11 +21651,17 @@ export type ProductType = Node & ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_PRODUCTS. */ availableAttributes: Maybe; - /** Whether the product type has variants. */ + /** + * Whether the product type has variants. + * @deprecated This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. + */ hasVariants: Scalars['Boolean']['output']; /** The ID of the product type. */ id: Scalars['ID']['output']; - /** Whether the product type is digital. */ + /** + * Whether the product type is digital - doesn't have any effect, it's present for backward-compatibility. + * @deprecated Will be removed in v3.24.0, use metadata or attributes instead. + */ isDigital: Scalars['Boolean']['output']; /** Whether shipping is required for this product type. */ isShippingRequired: Scalars['Boolean']['output']; @@ -22043,6 +21837,11 @@ export type ProductTypeEnum = | 'SHIPPABLE'; export type ProductTypeFilterInput = { + /** + * + * + * DEPRECATED: this field will be removed. The field has no effect on the API behavior. This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. + */ configurable?: InputMaybe; ids?: InputMaybe>; kind?: InputMaybe; @@ -22053,9 +21852,13 @@ export type ProductTypeFilterInput = { }; export type ProductTypeInput = { - /** Determines if product of this type has multiple variants. This option mainly simplifies product management in the dashboard. There is always at least one variant created under the hood. */ + /** + * Determines if product of this type has multiple variants. This option mainly simplifies product management in the dashboard. There is always at least one variant created under the hood. + * + * DEPRECATED: this field will be removed. The field has no effect on the API behavior. This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. + */ hasVariants?: InputMaybe; - /** Determines if products are digital. */ + /** Determines if products are digital - doesn't have any effect, it's present for backward-compatibility. */ isDigital?: InputMaybe; /** Determines if shipping is required for products of this variant. */ isShippingRequired?: InputMaybe; @@ -22188,12 +21991,6 @@ export type ProductVariant = Node & ObjectWithAttributes & ObjectWithMetadata & channelListings: Maybe>; /** The date and time when the product variant was created. */ created: Scalars['DateTime']['output']; - /** - * Digital content for the product variant. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - */ - digitalContent: Maybe; /** External ID of this product. */ externalReference: Maybe; /** The ID of the product variant. */ @@ -22593,7 +22390,9 @@ export type ProductVariantChannelListing = Node & { /** The price of the variant. */ price: Maybe; /** - * Prior price of the variant used for discount calculations. + * Previous price of the variant in channel. Useful for providing promotion information required by customer protection laws such as EU Omnibus directive. + * + * Warning: This field is not updated automatically. Use Channel Listings mutation to update it manually. * * Added in Saleor 3.21. */ @@ -24218,20 +24017,6 @@ export type Query = { * Requires one of the following permissions: MANAGE_ORDERS, MANAGE_USERS. */ customers: Maybe; - /** - * Look up digital content by ID. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContent: Maybe; - /** - * List of digital content. - * - * Requires one of the following permissions: MANAGE_PRODUCTS. - * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. - */ - digitalContents: Maybe; /** * List of draft orders. The query will not initiate any external requests, including filtering available shipping methods, or performing external tax calculations. * @@ -24559,7 +24344,7 @@ export type Query = { export type Query_EntitiesArgs = { - representations?: InputMaybe>>; + representations: Array; }; @@ -24707,19 +24492,6 @@ export type QueryCustomersArgs = { }; -export type QueryDigitalContentArgs = { - id: Scalars['ID']['input']; -}; - - -export type QueryDigitalContentsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - export type QueryDraftOrdersArgs = { after?: InputMaybe; before?: InputMaybe; @@ -25113,6 +24885,7 @@ export type QueryTransactionsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortBy?: InputMaybe; where?: InputMaybe; }; @@ -25257,7 +25030,7 @@ export type RefundSettingsErrorCode = export type RefundSettingsUpdate = { errors: Array; /** Refund settings. */ - refundSettings: RefundSettings; + refundSettings: Maybe; /** @deprecated Use `errors` field instead. */ refundSettingsErrors: Array; }; @@ -26008,7 +25781,10 @@ export type ShippingMethod = Node & ObjectWithMetadata & { id: Scalars['ID']['output']; /** Maximum delivery days for this shipping method. */ maximumDeliveryDays: Maybe; - /** Maximum order price for this shipping method. */ + /** + * Maximum order price for this shipping method. + * @deprecated No longer supported + */ maximumOrderPrice: Maybe; /** * Maximum order weight for this shipping method. @@ -26029,7 +25805,10 @@ export type ShippingMethod = Node & ObjectWithMetadata & { metafields: Maybe; /** Minimum delivery days for this shipping method. */ minimumDeliveryDays: Maybe; - /** Minimal order price for this shipping method. */ + /** + * Minimal order price for this shipping method. + * @deprecated No longer supported + */ minimumOrderPrice: Maybe; /** * Minimum order weight for this shipping method. @@ -26800,12 +26579,6 @@ export type Shop = ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_SETTINGS. */ allowLoginWithoutConfirmation: Maybe; - /** - * Enable automatic fulfillment for all digital products. - * - * Requires one of the following permissions: MANAGE_SETTINGS. - */ - automaticFulfillmentDigitalProducts: Maybe; /** List of available external authentications. */ availableExternalAuthentications: Array; /** List of available payment gateways. */ @@ -26839,18 +26612,6 @@ export type Shop = ObjectWithMetadata & { customerSetPasswordUrl: Maybe; /** Shop's default country. */ defaultCountry: Maybe; - /** - * Default number of max downloads per digital content URL. - * - * Requires one of the following permissions: MANAGE_SETTINGS. - */ - defaultDigitalMaxDownloads: Maybe; - /** - * Default number of days which digital content URL will be valid. - * - * Requires one of the following permissions: MANAGE_SETTINGS. - */ - defaultDigitalUrlValidDays: Maybe; /** * Default shop's email sender's address. * @@ -26920,6 +26681,12 @@ export type Shop = ObjectWithMetadata & { metafields: Maybe; /** Shop's name. */ name: Scalars['String']['output']; + /** + * Controls whether password-based authentication is allowed. + * + * Added in Saleor 3.23. + */ + passwordLoginMode: PasswordLoginModeEnum; /** List of available permissions. */ permissions: Array; /** List of possible phone prefixes. */ @@ -26966,6 +26733,12 @@ export type Shop = ObjectWithMetadata & { trackInventoryByDefault: Maybe; /** Returns translated shop fields for the given language code. */ translation: Maybe; + /** + * When enabled, stock availability is filtered by shipping zones and the destination address (legacy behavior). When disabled, stock availability is determined only by the direct warehouse-channel link, ignoring shipping zones. + * + * Added in Saleor 3.23. + */ + useLegacyShippingZoneStockAvailability: Scalars['Boolean']['output']; /** * Use legacy update webhook emission. When enabled, update webhooks (e.g. `customerUpdated`,`productVariantUpdated`) are sent even when only metadata changes. When disabled, update webhooks are not sent for metadata-only changes; only metadata-specific webhooks (e.g., `customerMetadataUpdated`, `productVariantMetadataUpdated`) are sent. * @@ -27073,6 +26846,7 @@ export type ShopErrorCode = | 'GRAPHQL_ERROR' | 'INVALID' | 'NOT_FOUND' + | 'PASSWORD_AUTH_RESTRICTION' | 'REQUIRED' | 'UNIQUE'; @@ -27106,8 +26880,6 @@ export type ShopMetadataUpdated = Event & { export type ShopSettingsInput = { /** Enable possibility to login without account confirmation. */ allowLoginWithoutConfirmation?: InputMaybe; - /** Enable automatic fulfillment for all digital products. */ - automaticFulfillmentDigitalProducts?: InputMaybe; /** * Charge taxes on shipping. * @@ -27116,10 +26888,6 @@ export type ShopSettingsInput = { chargeTaxesOnShipping?: InputMaybe; /** URL of a view where customers can set their password. */ customerSetPasswordUrl?: InputMaybe; - /** Default number of max downloads per digital content URL. */ - defaultDigitalMaxDownloads?: InputMaybe; - /** Default number of days which digital content URL will be valid. */ - defaultDigitalUrlValidDays?: InputMaybe; /** Default email sender's address. */ defaultMailSenderAddress?: InputMaybe; /** Default email sender's name. */ @@ -27156,6 +26924,12 @@ export type ShopSettingsInput = { * Warning: never store sensitive information, including financial data such as credit card details. */ metadata?: InputMaybe>; + /** + * Controls whether password-based authentication is allowed. + * + * Added in Saleor 3.23. + */ + passwordLoginMode?: InputMaybe; /** * When enabled, address fields that are not valid for a given country (according to Google's i18n address data) will be preserved instead of being removed during validation. Validation errors are still returned. * @@ -27174,6 +26948,12 @@ export type ShopSettingsInput = { reserveStockDurationAuthenticatedUser?: InputMaybe; /** This field is used as a default value for `ProductVariant.trackInventory`. */ trackInventoryByDefault?: InputMaybe; + /** + * When enabled, stock availability is filtered by shipping zones and the destination address (legacy behavior). When disabled, stock availability is determined only by the direct warehouse-channel link, ignoring shipping zones. + * + * Added in Saleor 3.23. + */ + useLegacyShippingZoneStockAvailability?: InputMaybe; /** * Use legacy update webhook emission. When enabled, update webhooks (e.g. `customerUpdated`,`productVariantUpdated`) are sent even when only metadata changes. When disabled, update webhooks are not sent for metadata-only changes; only metadata-specific webhooks (e.g., `customerMetadataUpdated`, `productVariantMetadataUpdated`) are sent. * @@ -28856,6 +28636,26 @@ export type TransactionEvent = Node & { type: Maybe; }; +/** + * Filter input for transaction events data. + * + * Added in Saleor 3.23. + */ +export type TransactionEventFilterInput = { + /** + * Filter transaction events by created at date. + * + * Added in Saleor 3.23. + */ + createdAt?: InputMaybe; + /** + * Filter transaction events by type. + * + * Added in Saleor 3.23. + */ + type?: InputMaybe; +}; + export type TransactionEventInput = { /** The message related to the event. */ message?: InputMaybe; @@ -28948,6 +28748,13 @@ export type TransactionEventTypeEnum = | 'REFUND_REVERSE' | 'REFUND_SUCCESS'; +export type TransactionEventTypeEnumFilterInput = { + /** The value equal to. */ + eq?: InputMaybe; + /** The value included in. */ + oneOf?: InputMaybe>; +}; + /** Filter input for transactions. */ export type TransactionFilterInput = { /** Filter by metadata fields of transactions. */ @@ -29297,6 +29104,27 @@ export type TransactionRequestRefundForGrantedRefundErrorCode = | 'REFUND_ALREADY_PROCESSED' | 'REFUND_IS_PENDING'; +export type TransactionSortField = + /** + * Sort transactions by creation date. + * + * Added in Saleor 3.23. + */ + | 'CREATED_AT' + /** + * Sort transactions by modification date. + * + * Added in Saleor 3.23. + */ + | 'MODIFIED_AT'; + +export type TransactionSortingInput = { + /** Specifies the direction in which to sort transactions. */ + direction: OrderDirection; + /** Sort transactions by the selected field. */ + field: TransactionSortField; +}; + /** * Update transaction. * @@ -29370,7 +29198,25 @@ export type TransactionWhereInput = { OR?: InputMaybe>; /** Filter by app identifier. */ appIdentifier?: InputMaybe; + /** + * Filter transactions by created at date. + * + * Added in Saleor 3.23. + */ + createdAt?: InputMaybe; + /** + * Filter by transaction events. Each list item represents conditions that must be satisfied by a single event. The filter matches transactions that have related events meeting all specified groups of conditions. + * + * Added in Saleor 3.23. + */ + events?: InputMaybe>; ids?: InputMaybe>; + /** + * Filter transactions by modified at date. + * + * Added in Saleor 3.23. + */ + modifiedAt?: InputMaybe; /** Filter by PSP reference. */ pspReference?: InputMaybe; }; @@ -29844,7 +29690,9 @@ export type UserSortField = /** Sort users by last name. */ | 'LAST_NAME' /** Sort users by order count. */ - | 'ORDER_COUNT'; + | 'ORDER_COUNT' + /** Sort users by rank. Note: This option is available only with the `search` filter. */ + | 'RANK'; export type UserSortingInput = { /** Specifies the direction in which to sort users. */ @@ -32059,15 +31907,6 @@ export type WeightUnitsEnum = | 'OZ' | 'TONNE'; -/** Represents the WIDGET target options for an app extension. */ -export type WidgetTargetOptions = { - /** - * HTTP method for Widget target (GET or POST) - * @deprecated Use `settings` field directly. - */ - method: HttpMethod; -}; - /** _Entity union as defined by Federation spec. */ export type _Entity = Address | App | Category | Collection | Group | Order | PageType | Product | ProductMedia | ProductType | ProductVariant | User; diff --git a/packages/infrastructure/src/acp/saleor/graphql/fragments/generated.ts b/packages/infrastructure/src/acp/saleor/graphql/fragments/generated.ts index 8bb1e5a6..9c33cef0 100644 --- a/packages/infrastructure/src/acp/saleor/graphql/fragments/generated.ts +++ b/packages/infrastructure/src/acp/saleor/graphql/fragments/generated.ts @@ -39,7 +39,7 @@ export type CheckoutSessionFragment_Checkout_lines_CheckoutLine_variant_ProductV export type CheckoutSessionFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type CheckoutSessionFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: CheckoutSessionFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type CheckoutSessionFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: CheckoutSessionFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type CheckoutSessionFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation = { name: string, plainText: string | null, richText: string | null }; @@ -98,7 +98,7 @@ export type CheckoutSessionFragment = { id: string, email: string | null, displa export type ProductFeedFragment_Product_media_ProductMedia = { url: string }; -export type ProductFeedFragment_Product_attributes_SelectedAttribute_attribute_Attribute = { id: string, name: string | null }; +export type ProductFeedFragment_Product_attributes_SelectedAttribute_attribute_Attribute = { id: string, name: string }; export type ProductFeedFragment_Product_attributes_SelectedAttribute_values_AttributeValue = { id: string, name: string | null, value: string | null }; @@ -110,7 +110,7 @@ export type ProductFeedFragment_Product_variants_ProductVariant_pricing_VariantP export type ProductFeedFragment_Product_variants_ProductVariant_pricing_VariantPricingInfo = { price: ProductFeedFragment_Product_variants_ProductVariant_pricing_VariantPricingInfo_price_TaxedMoney | null }; -export type ProductFeedFragment_Product_variants_ProductVariant_attributes_SelectedAttribute_attribute_Attribute = { id: string, name: string | null }; +export type ProductFeedFragment_Product_variants_ProductVariant_attributes_SelectedAttribute_attribute_Attribute = { id: string, name: string }; export type ProductFeedFragment_Product_variants_ProductVariant_attributes_SelectedAttribute_values_AttributeValue = { id: string, name: string | null, value: string | null }; diff --git a/packages/infrastructure/src/acp/saleor/graphql/mutations/generated.ts b/packages/infrastructure/src/acp/saleor/graphql/mutations/generated.ts index 373b3080..5c15fc6b 100644 --- a/packages/infrastructure/src/acp/saleor/graphql/mutations/generated.ts +++ b/packages/infrastructure/src/acp/saleor/graphql/mutations/generated.ts @@ -55,7 +55,7 @@ export type CheckoutSessionCreate_checkoutCreate_CheckoutCreate_checkout_Checkou export type CheckoutSessionCreate_checkoutCreate_CheckoutCreate_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type CheckoutSessionCreate_checkoutCreate_CheckoutCreate_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: CheckoutSessionCreate_checkoutCreate_CheckoutCreate_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type CheckoutSessionCreate_checkoutCreate_CheckoutCreate_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: CheckoutSessionCreate_checkoutCreate_CheckoutCreate_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type CheckoutSessionCreate_checkoutCreate_CheckoutCreate_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation = { name: string, plainText: string | null, richText: string | null }; diff --git a/packages/infrastructure/src/acp/saleor/graphql/queries/generated.ts b/packages/infrastructure/src/acp/saleor/graphql/queries/generated.ts index f0dc366e..72fbb6a0 100644 --- a/packages/infrastructure/src/acp/saleor/graphql/queries/generated.ts +++ b/packages/infrastructure/src/acp/saleor/graphql/queries/generated.ts @@ -39,7 +39,7 @@ export type CheckoutSessionGet_checkout_Checkout_lines_CheckoutLine_variant_Prod export type CheckoutSessionGet_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type CheckoutSessionGet_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: CheckoutSessionGet_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type CheckoutSessionGet_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: CheckoutSessionGet_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type CheckoutSessionGet_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation = { name: string, plainText: string | null, richText: string | null }; @@ -113,7 +113,7 @@ export type CheckoutSessionGet = CheckoutSessionGet_Query; export type ProductsFeedQuery_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_media_ProductMedia = { url: string }; -export type ProductsFeedQuery_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute = { id: string, name: string | null }; +export type ProductsFeedQuery_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute = { id: string, name: string }; export type ProductsFeedQuery_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_values_AttributeValue = { id: string, name: string | null, value: string | null }; @@ -125,7 +125,7 @@ export type ProductsFeedQuery_products_ProductCountableConnection_edges_ProductC export type ProductsFeedQuery_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_variants_ProductVariant_pricing_VariantPricingInfo = { price: ProductsFeedQuery_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_variants_ProductVariant_pricing_VariantPricingInfo_price_TaxedMoney | null }; -export type ProductsFeedQuery_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_variants_ProductVariant_attributes_SelectedAttribute_attribute_Attribute = { id: string, name: string | null }; +export type ProductsFeedQuery_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_variants_ProductVariant_attributes_SelectedAttribute_attribute_Attribute = { id: string, name: string }; export type ProductsFeedQuery_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_variants_ProductVariant_attributes_SelectedAttribute_values_AttributeValue = { id: string, name: string | null, value: string | null }; diff --git a/packages/infrastructure/src/cart/saleor/graphql/fragments/generated.ts b/packages/infrastructure/src/cart/saleor/graphql/fragments/generated.ts index a884d7c3..cb0e2334 100644 --- a/packages/infrastructure/src/cart/saleor/graphql/fragments/generated.ts +++ b/packages/infrastructure/src/cart/saleor/graphql/fragments/generated.ts @@ -25,7 +25,7 @@ export type CartFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_medi export type CartFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type CartFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: CartFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type CartFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: CartFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type CartFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation = { name: string, plainText: string | null, richText: string | null }; diff --git a/packages/infrastructure/src/cart/saleor/graphql/queries/generated.ts b/packages/infrastructure/src/cart/saleor/graphql/queries/generated.ts index e9a720b8..d219894b 100644 --- a/packages/infrastructure/src/cart/saleor/graphql/queries/generated.ts +++ b/packages/infrastructure/src/cart/saleor/graphql/queries/generated.ts @@ -25,7 +25,7 @@ export type CartQuery_checkout_Checkout_lines_CheckoutLine_variant_ProductVarian export type CartQuery_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type CartQuery_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: CartQuery_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type CartQuery_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: CartQuery_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type CartQuery_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation = { name: string, plainText: string | null, richText: string | null }; diff --git a/packages/infrastructure/src/checkout/saleor/graphql/fragments/generated.ts b/packages/infrastructure/src/checkout/saleor/graphql/fragments/generated.ts index c01ff300..1fd4a67d 100644 --- a/packages/infrastructure/src/checkout/saleor/graphql/fragments/generated.ts +++ b/packages/infrastructure/src/checkout/saleor/graphql/fragments/generated.ts @@ -41,7 +41,7 @@ export type CheckoutFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_ export type CheckoutFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type CheckoutFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: CheckoutFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type CheckoutFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: CheckoutFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type CheckoutFragment_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation = { name: string, plainText: string | null, richText: string | null }; @@ -108,9 +108,12 @@ export type CheckoutProblemsFragment_CheckoutLineProblemVariantNotAvailable = ( & { __typename: 'CheckoutLineProblemVariantNotAvailable' } ); +export type CheckoutProblemsFragment_CheckoutProblemDeliveryMethodInvalid_CheckoutProblemDeliveryMethodStale = Record; + export type CheckoutProblemsFragment = | CheckoutProblemsFragment_CheckoutLineProblemInsufficientStock | CheckoutProblemsFragment_CheckoutLineProblemVariantNotAvailable + | CheckoutProblemsFragment_CheckoutProblemDeliveryMethodInvalid_CheckoutProblemDeliveryMethodStale ; export class TypedDocumentString diff --git a/packages/infrastructure/src/checkout/saleor/graphql/queries/generated.ts b/packages/infrastructure/src/checkout/saleor/graphql/queries/generated.ts index 3a3a36de..7a990309 100644 --- a/packages/infrastructure/src/checkout/saleor/graphql/queries/generated.ts +++ b/packages/infrastructure/src/checkout/saleor/graphql/queries/generated.ts @@ -39,7 +39,7 @@ export type CheckoutFindQuery_checkout_Checkout_lines_CheckoutLine_variant_Produ export type CheckoutFindQuery_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type CheckoutFindQuery_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: CheckoutFindQuery_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type CheckoutFindQuery_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: CheckoutFindQuery_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type CheckoutFindQuery_checkout_Checkout_lines_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation = { name: string, plainText: string | null, richText: string | null }; diff --git a/packages/infrastructure/src/cms-menu/saleor/graphql/fragments/generated.ts b/packages/infrastructure/src/cms-menu/saleor/graphql/fragments/generated.ts index b1bf8025..d59fe931 100644 --- a/packages/infrastructure/src/cms-menu/saleor/graphql/fragments/generated.ts +++ b/packages/infrastructure/src/cms-menu/saleor/graphql/fragments/generated.ts @@ -7,7 +7,7 @@ export type MenuItem_MenuItem_category_Category_translation_CategoryTranslation export type MenuItem_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type MenuItem_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute = { name: string | null, slug: string | null, translation: MenuItem_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type MenuItem_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute = { name: string, slug: string, translation: MenuItem_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type MenuItem_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute = { attribute: MenuItem_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute }; diff --git a/packages/infrastructure/src/cms-menu/saleor/graphql/queries/generated.ts b/packages/infrastructure/src/cms-menu/saleor/graphql/queries/generated.ts index 7b92a58b..6b6625fc 100644 --- a/packages/infrastructure/src/cms-menu/saleor/graphql/queries/generated.ts +++ b/packages/infrastructure/src/cms-menu/saleor/graphql/queries/generated.ts @@ -7,7 +7,7 @@ export type MenuGet_menu_Menu_items_MenuItem_children_MenuItem_category_Category export type MenuGet_menu_Menu_items_MenuItem_children_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type MenuGet_menu_Menu_items_MenuItem_children_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute = { name: string | null, slug: string | null, translation: MenuGet_menu_Menu_items_MenuItem_children_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type MenuGet_menu_Menu_items_MenuItem_children_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute = { name: string, slug: string, translation: MenuGet_menu_Menu_items_MenuItem_children_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type MenuGet_menu_Menu_items_MenuItem_children_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute = { attribute: MenuGet_menu_Menu_items_MenuItem_children_MenuItem_category_Category_products_ProductCountableConnection_edges_ProductCountableEdge_node_Product_attributes_SelectedAttribute_attribute_Attribute }; diff --git a/packages/infrastructure/src/cms-page/saleor/graphql/queries/generated.ts b/packages/infrastructure/src/cms-page/saleor/graphql/queries/generated.ts index 36252a31..6adb4984 100644 --- a/packages/infrastructure/src/cms-page/saleor/graphql/queries/generated.ts +++ b/packages/infrastructure/src/cms-page/saleor/graphql/queries/generated.ts @@ -5,7 +5,7 @@ export type Page_page_Page_pageType_PageType = { slug: string }; export type Page_page_Page_attributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type Page_page_Page_attributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: Page_page_Page_attributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type Page_page_Page_attributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: Page_page_Page_attributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type Page_page_Page_attributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation = { name: string, plainText: string | null, richText: string | null }; diff --git a/packages/infrastructure/src/graphql/fragments/generated.ts b/packages/infrastructure/src/graphql/fragments/generated.ts index 86626de1..a02156d7 100644 --- a/packages/infrastructure/src/graphql/fragments/generated.ts +++ b/packages/infrastructure/src/graphql/fragments/generated.ts @@ -15,7 +15,7 @@ export type CartLineFragment_CheckoutLine_variant_ProductVariant_media_ProductMe export type CartLineFragment_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type CartLineFragment_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: CartLineFragment_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type CartLineFragment_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: CartLineFragment_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type CartLineFragment_CheckoutLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation = { name: string, plainText: string | null, richText: string | null }; diff --git a/packages/infrastructure/src/graphql/mutations/generated.ts b/packages/infrastructure/src/graphql/mutations/generated.ts index 8f2157b9..882aa644 100644 --- a/packages/infrastructure/src/graphql/mutations/generated.ts +++ b/packages/infrastructure/src/graphql/mutations/generated.ts @@ -3,13 +3,13 @@ import type * as Types from '@nimara/codegen/schema'; import type { DocumentTypeDecoration } from '@graphql-typed-document-node/core'; export type MetadataUpdateMutation_updateMetadata_UpdateMetadata_item_ObjectWithMetadata_metadata_MetadataItem = { key: string, value: string }; -export type MetadataUpdateMutation_updateMetadata_UpdateMetadata_item_WyJ2yV8kbV43ZuNk2LnMpdht4wzJk1vzx9OO77bTYA = { metadata: Array }; +export type MetadataUpdateMutation_updateMetadata_UpdateMetadata_item_t4LZBTNEioGoCF33HyRMtk1PP0lILn2Vy2fZeczKY = { metadata: Array }; -export type MetadataUpdateMutation_updateMetadata_UpdateMetadata_item_NoYDRO3cg6qyD5DB6BzM4ezHkwXTKXAXyLqRKqBaUE = { metadata: Array }; +export type MetadataUpdateMutation_updateMetadata_UpdateMetadata_item_mxPBzT6KxFUZiblhuYBU9JhNNEafJgs5OwglGcov8 = { metadata: Array }; export type MetadataUpdateMutation_updateMetadata_UpdateMetadata_item = - | MetadataUpdateMutation_updateMetadata_UpdateMetadata_item_WyJ2yV8kbV43ZuNk2LnMpdht4wzJk1vzx9OO77bTYA - | MetadataUpdateMutation_updateMetadata_UpdateMetadata_item_NoYDRO3cg6qyD5DB6BzM4ezHkwXTKXAXyLqRKqBaUE + | MetadataUpdateMutation_updateMetadata_UpdateMetadata_item_t4LZBTNEioGoCF33HyRMtk1PP0lILn2Vy2fZeczKY + | MetadataUpdateMutation_updateMetadata_UpdateMetadata_item_mxPBzT6KxFUZiblhuYBU9JhNNEafJgs5OwglGcov8 ; export type MetadataUpdateMutation_updateMetadata_UpdateMetadata_errors_MetadataError = { field: string | null, code: Types.MetadataErrorCode, message: string | null }; diff --git a/packages/infrastructure/src/search/saleor/graphql/queries/generated.ts b/packages/infrastructure/src/search/saleor/graphql/queries/generated.ts index 8963470e..0a2c6d06 100644 --- a/packages/infrastructure/src/search/saleor/graphql/queries/generated.ts +++ b/packages/infrastructure/src/search/saleor/graphql/queries/generated.ts @@ -13,7 +13,7 @@ export type FacetsQuery_attributes_AttributeCountableConnection_edges_AttributeC export type FacetsQuery_attributes_AttributeCountableConnection_edges_AttributeCountableEdge_node_Attribute_choices_AttributeValueCountableConnection = { totalCount: number | null, edges: Array, pageInfo: FacetsQuery_attributes_AttributeCountableConnection_edges_AttributeCountableEdge_node_Attribute_choices_AttributeValueCountableConnection_pageInfo_PageInfo }; -export type FacetsQuery_attributes_AttributeCountableConnection_edges_AttributeCountableEdge_node_Attribute = { name: string | null, slug: string | null, inputType: Types.AttributeInputTypeEnum | null, withChoices: boolean, translation: FacetsQuery_attributes_AttributeCountableConnection_edges_AttributeCountableEdge_node_Attribute_translation_AttributeTranslation | null, choices: FacetsQuery_attributes_AttributeCountableConnection_edges_AttributeCountableEdge_node_Attribute_choices_AttributeValueCountableConnection | null }; +export type FacetsQuery_attributes_AttributeCountableConnection_edges_AttributeCountableEdge_node_Attribute = { name: string, slug: string, inputType: Types.AttributeInputTypeEnum | null, withChoices: boolean, translation: FacetsQuery_attributes_AttributeCountableConnection_edges_AttributeCountableEdge_node_Attribute_translation_AttributeTranslation | null, choices: FacetsQuery_attributes_AttributeCountableConnection_edges_AttributeCountableEdge_node_Attribute_choices_AttributeValueCountableConnection | null }; export type FacetsQuery_attributes_AttributeCountableConnection_edges_AttributeCountableEdge = { node: FacetsQuery_attributes_AttributeCountableConnection_edges_AttributeCountableEdge_node_Attribute }; diff --git a/packages/infrastructure/src/store/saleor/graphql/fragments/generated.ts b/packages/infrastructure/src/store/saleor/graphql/fragments/generated.ts index cf2adf68..a9043ca4 100644 --- a/packages/infrastructure/src/store/saleor/graphql/fragments/generated.ts +++ b/packages/infrastructure/src/store/saleor/graphql/fragments/generated.ts @@ -3,7 +3,7 @@ import type * as Types from '@nimara/codegen/schema'; import type { DocumentTypeDecoration } from '@graphql-typed-document-node/core'; export type AttributeFragment_Attribute_translation_AttributeTranslation = { name: string }; -export type AttributeFragment = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: AttributeFragment_Attribute_translation_AttributeTranslation | null }; +export type AttributeFragment = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: AttributeFragment_Attribute_translation_AttributeTranslation | null }; export type AttributeValueFragment_AttributeValue_translation_AttributeValueTranslation = { name: string, plainText: string | null, richText: string | null }; @@ -47,7 +47,7 @@ export type ProductDetailsFragment_Product_variants_ProductVariant_translation_P export type ProductDetailsFragment_Product_variants_ProductVariant_media_ProductMedia = { url: string, alt: string, type: Types.ProductMediaType }; -export type ProductDetailsFragment_Product_variants_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: AttributeFragment_Attribute_translation_AttributeTranslation | null }; +export type ProductDetailsFragment_Product_variants_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: AttributeFragment_Attribute_translation_AttributeTranslation | null }; export type ProductDetailsFragment_Product_variants_ProductVariant_selectionAttributes_SelectedAttribute_values_AttributeValue = { slug: string | null, name: string | null, plainText: string | null, richText: string | null, boolean: boolean | null, date: string | null, dateTime: string | null, reference: string | null, value: string | null, translation: AttributeValueFragment_AttributeValue_translation_AttributeValueTranslation | null, file: AttributeValueFragment_AttributeValue_file_File | null }; @@ -57,7 +57,7 @@ export type ProductDetailsFragment_Product_variants_ProductVariant_nonSelectionA export type ProductDetailsFragment_Product_variants_ProductVariant = { id: string, name: string, translation: ProductDetailsFragment_Product_variants_ProductVariant_translation_ProductVariantTranslation | null, media: Array | null, selectionAttributes: Array, nonSelectionAttributes: Array }; -export type ProductDetailsFragment_Product_attributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: AttributeFragment_Attribute_translation_AttributeTranslation | null }; +export type ProductDetailsFragment_Product_attributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: AttributeFragment_Attribute_translation_AttributeTranslation | null }; export type ProductDetailsFragment_Product_attributes_SelectedAttribute_values_AttributeValue = { slug: string | null, name: string | null, plainText: string | null, richText: string | null, boolean: boolean | null, date: string | null, dateTime: string | null, reference: string | null, value: string | null, translation: AttributeValueFragment_AttributeValue_translation_AttributeValueTranslation | null, file: AttributeValueFragment_AttributeValue_file_File | null }; diff --git a/packages/infrastructure/src/store/saleor/graphql/queries/generated.ts b/packages/infrastructure/src/store/saleor/graphql/queries/generated.ts index 4a8720cc..83f3cd35 100644 --- a/packages/infrastructure/src/store/saleor/graphql/queries/generated.ts +++ b/packages/infrastructure/src/store/saleor/graphql/queries/generated.ts @@ -5,7 +5,7 @@ export type PageSlugByIdQuery_page_Page_translation_PageTranslation = { title: s export type PageSlugByIdQuery_page_Page_pageType_PageType = { slug: string }; -export type PageSlugByIdQuery_page_Page_attributes_SelectedAttribute_attribute_Attribute = { slug: string | null }; +export type PageSlugByIdQuery_page_Page_attributes_SelectedAttribute_attribute_Attribute = { slug: string }; export type PageSlugByIdQuery_page_Page_attributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation = { name: string, plainText: string | null, richText: string | null }; @@ -90,7 +90,7 @@ export type ProductDetailsQuery_product_Product_variants_ProductVariant_media_Pr export type ProductDetailsQuery_product_Product_variants_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type ProductDetailsQuery_product_Product_variants_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: ProductDetailsQuery_product_Product_variants_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type ProductDetailsQuery_product_Product_variants_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: ProductDetailsQuery_product_Product_variants_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type ProductDetailsQuery_product_Product_variants_ProductVariant_selectionAttributes_SelectedAttribute_values_AttributeValue = { slug: string | null, name: string | null, plainText: string | null, richText: string | null, boolean: boolean | null, date: string | null, dateTime: string | null, reference: string | null, value: string | null, translation: PageSlugByIdQuery_page_Page_attributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation | null, file: PageSlugByIdQuery_page_Page_attributes_SelectedAttribute_values_AttributeValue_file_File | null }; @@ -100,7 +100,7 @@ export type ProductDetailsQuery_product_Product_variants_ProductVariant_nonSelec export type ProductDetailsQuery_product_Product_variants_ProductVariant = { id: string, name: string, translation: ProductDetailsQuery_product_Product_variants_ProductVariant_translation_ProductVariantTranslation | null, media: Array | null, selectionAttributes: Array, nonSelectionAttributes: Array }; -export type ProductDetailsQuery_product_Product_attributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: ProductDetailsQuery_product_Product_variants_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type ProductDetailsQuery_product_Product_attributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: ProductDetailsQuery_product_Product_variants_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type ProductDetailsQuery_product_Product_attributes_SelectedAttribute_values_AttributeValue = { slug: string | null, name: string | null, plainText: string | null, richText: string | null, boolean: boolean | null, date: string | null, dateTime: string | null, reference: string | null, value: string | null, translation: PageSlugByIdQuery_page_Page_attributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation | null, file: PageSlugByIdQuery_page_Page_attributes_SelectedAttribute_values_AttributeValue_file_File | null }; diff --git a/packages/infrastructure/src/user/saleor/graphql/fragments/generated.ts b/packages/infrastructure/src/user/saleor/graphql/fragments/generated.ts index c7ced0e3..b6140600 100644 --- a/packages/infrastructure/src/user/saleor/graphql/fragments/generated.ts +++ b/packages/infrastructure/src/user/saleor/graphql/fragments/generated.ts @@ -17,7 +17,7 @@ export type OrderFragment_Order_lines_OrderLine_thumbnail_Image = { url: string, export type OrderFragment_Order_lines_OrderLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type OrderFragment_Order_lines_OrderLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: OrderFragment_Order_lines_OrderLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type OrderFragment_Order_lines_OrderLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: OrderFragment_Order_lines_OrderLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type OrderFragment_Order_lines_OrderLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation = { name: string, plainText: string | null, richText: string | null }; diff --git a/packages/infrastructure/src/user/saleor/graphql/queries/generated.ts b/packages/infrastructure/src/user/saleor/graphql/queries/generated.ts index 114be346..e420ebd7 100644 --- a/packages/infrastructure/src/user/saleor/graphql/queries/generated.ts +++ b/packages/infrastructure/src/user/saleor/graphql/queries/generated.ts @@ -51,7 +51,7 @@ export type UserOrdersQuery_me_User_orders_OrderCountableConnection_edges_OrderC export type UserOrdersQuery_me_User_orders_OrderCountableConnection_edges_OrderCountableEdge_node_Order_lines_OrderLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation = { name: string }; -export type UserOrdersQuery_me_User_orders_OrderCountableConnection_edges_OrderCountableEdge_node_Order_lines_OrderLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string | null, inputType: Types.AttributeInputTypeEnum | null, name: string | null, translation: UserOrdersQuery_me_User_orders_OrderCountableConnection_edges_OrderCountableEdge_node_Order_lines_OrderLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; +export type UserOrdersQuery_me_User_orders_OrderCountableConnection_edges_OrderCountableEdge_node_Order_lines_OrderLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute = { slug: string, inputType: Types.AttributeInputTypeEnum | null, name: string, translation: UserOrdersQuery_me_User_orders_OrderCountableConnection_edges_OrderCountableEdge_node_Order_lines_OrderLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_attribute_Attribute_translation_AttributeTranslation | null }; export type UserOrdersQuery_me_User_orders_OrderCountableConnection_edges_OrderCountableEdge_node_Order_lines_OrderLine_variant_ProductVariant_selectionAttributes_SelectedAttribute_values_AttributeValue_translation_AttributeValueTranslation = { name: string, plainText: string | null, richText: string | null }; From c00df2073b2bef12f1a2d18eaa4d01bddb5cd0a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bogda=C5=82?= Date: Fri, 17 Apr 2026 00:32:06 +0200 Subject: [PATCH 14/23] feat: add ledger for vendor payouts --- AGENTS.md | 14 +- apps/marketplace/.env.example | 5 + apps/marketplace/docker-compose.yml | 20 + apps/marketplace/package.json | 4 + apps/marketplace/scripts/migrate-ledger.mjs | 34 + .../_actions/stripe-connect.ts | 36 + .../app/api/payments/stripe/webhooks/route.ts | 349 ++--- .../api/payouts/batches/[id]/execute/route.ts | 32 + .../src/app/api/payouts/batches/[id]/route.ts | 59 + .../app/api/payouts/batches/close/route.ts | 73 ++ .../api/payouts/ledger/sync-stripe/route.ts | 53 + .../src/app/api/payouts/overview/route.ts | 114 ++ .../src/app/api/saleor/manifest/route.ts | 25 + .../api/saleor/webhooks/order-paid/route.ts | 345 +++++ .../app/api/stripe/connect/webhook/route.ts | 213 +++- .../app/app/_components/app-page-client.tsx | 28 +- .../_components/app-payouts-overview-tab.tsx | 1123 +++++++++++++++++ apps/marketplace/src/lib/auth/server.ts | 52 +- apps/marketplace/src/lib/config.ts | 26 + .../src/lib/graphql/server/schema.ts | 111 ++ .../src/lib/ledger/close-payout-batch.ts | 48 + .../src/lib/ledger/execute-payout-batch.ts | 131 ++ .../src/lib/ledger/ingest-order-paid.ts | 47 + .../lib/ledger/link-orders-stripe-charge.ts | 85 ++ apps/marketplace/src/lib/ledger/pool.ts | 19 + apps/marketplace/src/lib/ledger/repository.ts | 843 +++++++++++++ .../sync-ledger-settlement-from-stripe.ts | 129 ++ apps/marketplace/src/lib/saleor/consts.ts | 2 + .../src/lib/saleor/fetch-order-for-ledger.ts | 206 +++ .../lib/saleor/fetch-vendor-page-titles.ts | 34 + .../src/lib/stripe/account-updated-webhook.ts | 99 ++ apps/marketplace/src/lib/stripe/client.ts | 42 + apps/marketplace/src/lib/stripe/connect.ts | 2 + apps/marketplace/src/lib/stripe/payout-api.ts | 152 +++ .../stripe/process-ledger-stripe-webhook.ts | 180 +++ .../src/providers/auth-provider.tsx | 12 + package.json | 4 +- .../domain/src/objects/LedgerSettlement.ts | 37 + .../i18n/src/messages/en/marketplace.json | 76 ++ pnpm-lock.yaml | 62 +- turbo.json | 4 +- 41 files changed, 4701 insertions(+), 229 deletions(-) create mode 100644 apps/marketplace/.env.example create mode 100644 apps/marketplace/scripts/migrate-ledger.mjs create mode 100644 apps/marketplace/src/app/api/payouts/batches/[id]/execute/route.ts create mode 100644 apps/marketplace/src/app/api/payouts/batches/[id]/route.ts create mode 100644 apps/marketplace/src/app/api/payouts/batches/close/route.ts create mode 100644 apps/marketplace/src/app/api/payouts/ledger/sync-stripe/route.ts create mode 100644 apps/marketplace/src/app/api/payouts/overview/route.ts create mode 100644 apps/marketplace/src/app/api/saleor/webhooks/order-paid/route.ts create mode 100644 apps/marketplace/src/app/app/_components/app-payouts-overview-tab.tsx create mode 100644 apps/marketplace/src/lib/ledger/close-payout-batch.ts create mode 100644 apps/marketplace/src/lib/ledger/execute-payout-batch.ts create mode 100644 apps/marketplace/src/lib/ledger/ingest-order-paid.ts create mode 100644 apps/marketplace/src/lib/ledger/link-orders-stripe-charge.ts create mode 100644 apps/marketplace/src/lib/ledger/pool.ts create mode 100644 apps/marketplace/src/lib/ledger/repository.ts create mode 100644 apps/marketplace/src/lib/ledger/sync-ledger-settlement-from-stripe.ts create mode 100644 apps/marketplace/src/lib/saleor/fetch-order-for-ledger.ts create mode 100644 apps/marketplace/src/lib/saleor/fetch-vendor-page-titles.ts create mode 100644 apps/marketplace/src/lib/stripe/account-updated-webhook.ts create mode 100644 apps/marketplace/src/lib/stripe/payout-api.ts create mode 100644 apps/marketplace/src/lib/stripe/process-ledger-stripe-webhook.ts create mode 100644 packages/domain/src/objects/LedgerSettlement.ts diff --git a/AGENTS.md b/AGENTS.md index b5fa65d9..af5f7b06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,8 @@ Nimara uses a **layered monorepo** with strict dependency boundaries to keep cod ``` apps/ ├── storefront/ # Next.js 16 customer storefront -├── stripe/ # Stripe payment integration app +├── marketplace/ # Vendor dashboard; Postgres ledger, Stripe Connect, payout batches +├── stripe/ # Stripe payment integration app (Saleor Payment Gateway) └── automated-tests/ # Playwright E2E tests packages/ @@ -91,6 +92,13 @@ ui (leaf) ├→ features ──→ apps For decision tree and detailed scenarios, see `.agents/skills/project-guidelines/SKILL.md`. +### Marketplace (`apps/marketplace`) + +- **Role:** Multi-vendor operations UI (products, orders, configuration) against Saleor via stitched GraphQL; vendors authenticate with JWT (`src/lib/auth/`, `src/providers/auth-provider.tsx`). +- **Ledger & payouts (Postgres):** Optional `DATABASE_URL`. Apply schema with `node apps/marketplace/scripts/migrate-ledger.mjs` (runs all `apps/marketplace/db/migrations/*.sql` in order). Core tables: `ledger_entries` (idempotent lines, `consumed_in_batch_id` ties lines to a closed batch), `payout_batches` / `payout_batch_items`, `stripe_transfers` (Stripe **Transfer** to Connect per batch line). Settlement stops at Transfers; **Stripe Payout** objects (bank withdrawal from Connect) are not persisted. +- **APIs:** Stripe Connect webhooks, payment/PI routes, Saleor webhooks (e.g. order-paid → ledger ingest), payout overview / close / execute batch — see `src/app/api/` and `src/lib/ledger/`. +- **Storefront coupling:** Marketplace checkout flow uses vendor metadata and Stripe Connect; see storefront checkout/cart changes that reference marketplace payment URLs when configured. + --- ## 1. Lead Developer @@ -109,6 +117,7 @@ For decision tree and detailed scenarios, see `.agents/skills/project-guidelines 1. **Monorepo layout** - `apps/storefront` — Next.js 16 storefront (main app). + - `apps/marketplace` — Vendor dashboard; Saleor GraphQL stitching, Stripe Connect, optional Postgres ledger and batched Connect Transfers (`src/lib/ledger/`). - `apps/stripe` — Next.js Stripe payment app (Saleor Payment Gateway). - `apps/docs` — Nextra docs; `apps/automated-tests` — Playwright e2e. - `packages/domain` — shared types/objects/consts only (leaf package). @@ -215,6 +224,9 @@ For decision tree and detailed scenarios, see `.agents/skills/project-guidelines 4. **Sentry** - Storefront and Stripe use Sentry; config in `sentry.*.config.ts`. Error service in storefront sets user context on server and passes minimal user info to client for reporting. +5. **Marketplace ledger (Postgres)** + - Requires `DATABASE_URL` (see root / `apps/marketplace` `.env.example`). Apply migrations: `node apps/marketplace/scripts/migrate-ledger.mjs` from repo root with env loaded. Schema lives in `apps/marketplace/db/migrations/` (typically a single init file for greenfield; idempotent `create if not exists` where appropriate). + --- ## 4. QA / Testing diff --git a/apps/marketplace/.env.example b/apps/marketplace/.env.example new file mode 100644 index 00000000..72e408bc --- /dev/null +++ b/apps/marketplace/.env.example @@ -0,0 +1,5 @@ +# Copy to `.env` and adjust. Many marketplace vars also live in the monorepo root `.env.example`. + +# --- Ledger / payout (local Postgres; see docker-compose.yml) --- +# After: `docker compose up -d` from this directory +DATABASE_URL=postgresql://marketplace:marketplace@127.0.0.1:5434/marketplace_ledger diff --git a/apps/marketplace/docker-compose.yml b/apps/marketplace/docker-compose.yml index 4453ebf7..77618179 100644 --- a/apps/marketplace/docker-compose.yml +++ b/apps/marketplace/docker-compose.yml @@ -1,4 +1,21 @@ services: + db-ledger: + container_name: marketplace-db-ledger + image: postgres:16-alpine + ports: + - "127.0.0.1:5434:5432" + environment: + - POSTGRES_USER=marketplace + - POSTGRES_PASSWORD=marketplace + - POSTGRES_DB=marketplace_ledger + volumes: + - marketplace-ledger-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U marketplace -d marketplace_ledger"] + interval: 5s + timeout: 5s + retries: 10 + # LocalStack for AWS Services localstack: container_name: marketplace-localstack @@ -25,3 +42,6 @@ services: interval: 10s timeout: 5s retries: 5 + +volumes: + marketplace-ledger-data: diff --git a/apps/marketplace/package.json b/apps/marketplace/package.json index 01ec6fb2..b8008d6d 100644 --- a/apps/marketplace/package.json +++ b/apps/marketplace/package.json @@ -33,6 +33,7 @@ "next": "^16.1.6", "next-intl": "4.8.3", "nodemailer": "8.0.2", + "pg": "^8.16.0", "react": "^19.2.3", "react-dom": "^19.2.3", "react-hook-form": "^7.71.2", @@ -42,6 +43,7 @@ "devDependencies": { "@types/node": "^22.19.15", "@types/nodemailer": "7.0.11", + "@types/pg": "^8.15.5", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "autoprefixer": "10.4.27", @@ -64,6 +66,8 @@ "lint:fix": "eslint . --ext .js,.ts,.tsx --fix", "localstack:down": "docker-compose down", "localstack:up": "docker-compose up -d localstack", + "db-ledger:up": "docker-compose up -d db-ledger", + "db-ledger:down": "docker-compose stop db-ledger", "start": "next start --port 3001", "test": "vitest run", "test:coverage": "vitest run --coverage", diff --git a/apps/marketplace/scripts/migrate-ledger.mjs b/apps/marketplace/scripts/migrate-ledger.mjs new file mode 100644 index 00000000..ddb6493a --- /dev/null +++ b/apps/marketplace/scripts/migrate-ledger.mjs @@ -0,0 +1,34 @@ +import { readFileSync, readdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import pg from "pg"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const connectionString = process.env.DATABASE_URL?.trim(); + +if (!connectionString) { + console.error("DATABASE_URL is required (see apps/marketplace/.env.example)"); + process.exit(1); +} + +const migrationsDir = join(__dirname, "../db/migrations"); +const files = readdirSync(migrationsDir) + .filter((f) => f.endsWith(".sql")) + .sort(); + +const client = new pg.Client({ connectionString }); + +await client.connect(); + +try { + for (const file of files) { + const sqlPath = join(migrationsDir, file); + const sql = readFileSync(sqlPath, "utf8"); + + await client.query(sql); + console.log("Ledger migration applied:", sqlPath); + } +} finally { + await client.end(); +} diff --git a/apps/marketplace/src/app/(authenticated)/_actions/stripe-connect.ts b/apps/marketplace/src/app/(authenticated)/_actions/stripe-connect.ts index 3ae3af87..38884965 100644 --- a/apps/marketplace/src/app/(authenticated)/_actions/stripe-connect.ts +++ b/apps/marketplace/src/app/(authenticated)/_actions/stripe-connect.ts @@ -2,6 +2,8 @@ import { getServerAuthToken, getServerVendorId } from "@/lib/auth/server"; import { config } from "@/lib/config"; +import { getLedgerPool } from "@/lib/ledger/pool"; +import { upsertVendorStripeAccount } from "@/lib/ledger/repository"; import { METADATA_KEYS } from "@/lib/saleor/consts"; import { getVendorPageMetadata, @@ -109,6 +111,23 @@ export async function createStripeConnectOnboardingSession(): Promise { ]), }); + const pool = getLedgerPool(); + + if (pool) { + const defaultCurrency = + typeof account.default_currency === "string" + ? account.default_currency + : "usd"; + + await upsertVendorStripeAccount(pool, { + defaultCurrency, + onboardingCompleted: connected, + payoutsEnabled: Boolean(account.payouts_enabled), + stripeAccountId: paymentMetadata.paymentAccountId, + vendorId: vendorPageId, + }); + } + return { ok: true, connected, diff --git a/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts b/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts index 81fbc26f..d4463391 100644 --- a/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts +++ b/apps/marketplace/src/app/api/payments/stripe/webhooks/route.ts @@ -1,6 +1,7 @@ import { type NextRequest, NextResponse } from "next/server"; import type { TransactionCreateMutationVariables } from "@/graphql/generated/client"; +import { linkOrdersToStripeChargeFromPaymentIntent } from "@/lib/ledger/link-orders-stripe-charge"; import { getAppConfig } from "@/lib/saleor/app-config"; import { verifyStripeWebhookSignatureDetailed } from "@/lib/stripe/webhook-signature"; import { checkoutService } from "@/services/checkouts"; @@ -33,6 +34,12 @@ type ServiceError = { message: string; }; +type CheckoutStripeProcessingResult = { + errors: ServiceError[]; + orderId: string | null; + transaction: { id: string; name: string } | null; +}; + type TransactionCreatePayload = { errors: Array<{ code: string; message: string | null }>; transaction: { id: string; name: string } | null; @@ -283,7 +290,7 @@ export async function POST(request: NextRequest) { const completeCheckout = async ( checkoutId: string, - ): Promise => { + ): Promise<{ errors: ServiceError[]; orderId: string | null }> => { const checkoutCompleteResult = await checkoutService.completeCheckout( { id: checkoutId }, config.authToken, @@ -298,19 +305,22 @@ export async function POST(request: NextRequest) { mappedCheckoutCompleteErrors.length === 0 && checkoutCompleteResult.errors.length > 0 ) { - return []; + return { errors: [], orderId: null }; } if (mappedCheckoutCompleteErrors.length === 0) { - return [ - { - code: "UNKNOWN_CHECKOUT_COMPLETE_ERROR", - message: "checkoutComplete failed without error details.", - }, - ]; + return { + errors: [ + { + code: "UNKNOWN_CHECKOUT_COMPLETE_ERROR", + message: "checkoutComplete failed without error details.", + }, + ], + orderId: null, + }; } - return mappedCheckoutCompleteErrors; + return { errors: mappedCheckoutCompleteErrors, orderId: null }; } const checkoutCompleteResultData = @@ -325,178 +335,209 @@ export async function POST(request: NextRequest) { rawCheckoutCompleteErrors.every((error) => isCheckoutCompleteNotFoundError(error.code), ); + const orderId = + checkoutCompleteResultData.checkoutComplete?.order?.id ?? null; if (mappedCheckoutCompleteErrors.length > 0) { - return mappedCheckoutCompleteErrors; + return { errors: mappedCheckoutCompleteErrors, orderId: null }; } if ( checkoutCompleteResultData.checkoutComplete?.order || hasOnlyNotFoundErrors ) { - return []; + return { errors: [], orderId }; } - return [ - { - code: "UNKNOWN_CHECKOUT_COMPLETE_ERROR", - message: "checkoutComplete returned no order and no error details.", - }, - ]; + return { + errors: [ + { + code: "UNKNOWN_CHECKOUT_COMPLETE_ERROR", + message: "checkoutComplete returned no order and no error details.", + }, + ], + orderId: null, + }; }; const settled = await Promise.allSettled( - checkoutIds.map(async (checkoutId) => { - const amount = checkoutAmounts[checkoutId]; - const checkoutTransactionsResult = - await transactionsService.getCheckoutTransactions( - { id: checkoutId }, - config.authToken, - ); - - if (!checkoutTransactionsResult.ok) { - return { - transaction: null, - errors: checkoutTransactionsResult.errors.map((error) => ({ - code: error.code, - message: error.message ?? "checkout query failed.", - })), - }; - } + checkoutIds.map( + async (checkoutId): Promise => { + const amount = checkoutAmounts[checkoutId]; + const checkoutTransactionsResult = + await transactionsService.getCheckoutTransactions( + { id: checkoutId }, + config.authToken, + ); + + if (!checkoutTransactionsResult.ok) { + return { + transaction: null, + errors: checkoutTransactionsResult.errors.map((error) => ({ + code: error.code, + message: error.message ?? "checkout query failed.", + })), + orderId: null, + }; + } - const checkoutTransactionsData = - checkoutTransactionsResult.data as CheckoutTransactionsPayload; - const alreadyCharged = - checkoutTransactionsData.checkout?.transactions?.some((transaction) => { - if (transaction.pspReference !== paymentIntentId) { - return false; + const checkoutTransactionsData = + checkoutTransactionsResult.data as CheckoutTransactionsPayload; + const alreadyCharged = + checkoutTransactionsData.checkout?.transactions?.some( + (transaction) => { + if (transaction.pspReference !== paymentIntentId) { + return false; + } + + return (transaction.chargedAmount?.amount ?? 0) > 0; + }, + ) ?? false; + + if (alreadyCharged) { + marketplaceLogger.warning( + "Stripe webhook recovery path: checkout already charged, retrying checkoutComplete.", + { + checkoutId, + eventId: event.id, + paymentIntentId, + saleorDomain, + }, + ); + + let checkoutCompleteOutcome: Awaited< + ReturnType + >; + + try { + checkoutCompleteOutcome = await completeCheckout(checkoutId); + } catch (error) { + return { + transaction: null, + errors: mapCheckoutCompleteRequestFailure(error), + orderId: null, + }; } - return (transaction.chargedAmount?.amount ?? 0) > 0; - }) ?? false; - - if (alreadyCharged) { - marketplaceLogger.warning( - "Stripe webhook recovery path: checkout already charged, retrying checkoutComplete.", - { - checkoutId, - eventId: event.id, - paymentIntentId, - saleorDomain, - }, - ); - - let checkoutCompleteErrors: ServiceError[]; - - try { - checkoutCompleteErrors = await completeCheckout(checkoutId); - } catch (error) { - checkoutCompleteErrors = mapCheckoutCompleteRequestFailure(error); - } + if (checkoutCompleteOutcome.errors.length > 0) { + return { + transaction: null, + errors: checkoutCompleteOutcome.errors, + orderId: null, + }; + } - if (checkoutCompleteErrors.length > 0) { return { - transaction: null, - errors: checkoutCompleteErrors, + transaction: { + id: "already-processed", + name: "PaymentIntent Succeeded", + }, + errors: [], + orderId: checkoutCompleteOutcome.orderId, }; } - return { + const transactionVariables: TransactionCreateMutationVariables = { + id: checkoutId, transaction: { - id: "already-processed", name: "PaymentIntent Succeeded", + amountCharged: { + amount, + currency, + }, + pspReference: paymentIntentId, }, - errors: [], - }; - } - - const transactionVariables: TransactionCreateMutationVariables = { - id: checkoutId, - transaction: { - name: "PaymentIntent Succeeded", - amountCharged: { - amount, - currency, + transactionEvent: { + pspReference: paymentIntentId, + message: "Payment successful", }, - pspReference: paymentIntentId, - }, - transactionEvent: { - pspReference: paymentIntentId, - message: "Payment successful", - }, - }; - const transactionCreateResult = - await transactionsService.createTransaction( - transactionVariables, - config.authToken, - ); - - if (!transactionCreateResult.ok) { - return { - transaction: null, - errors: transactionCreateResult.errors.map((error) => ({ - code: error.code, - message: error.message ?? "transactionCreate failed.", - })), }; - } + const transactionCreateResult = + await transactionsService.createTransaction( + transactionVariables, + config.authToken, + ); - const transactionCreateResultData = transactionCreateResult.data as - | TransactionCreatePayload - | { transactionCreate: TransactionCreatePayload | null }; - const transactionCreatePayload = - "transactionCreate" in transactionCreateResultData - ? transactionCreateResultData.transactionCreate - : transactionCreateResultData; + if (!transactionCreateResult.ok) { + return { + transaction: null, + errors: transactionCreateResult.errors.map((error) => ({ + code: error.code, + message: error.message ?? "transactionCreate failed.", + })), + orderId: null, + }; + } - if ( - !transactionCreatePayload?.transaction || - transactionCreatePayload.errors.length > 0 - ) { - const mappedErrors = transactionCreatePayload?.errors.map((error) => ({ - code: error.code, - message: error.message ?? "Unknown transactionCreate error.", - })); + const transactionCreateResultData = transactionCreateResult.data as + | TransactionCreatePayload + | { transactionCreate: TransactionCreatePayload | null }; + const transactionCreatePayload = + "transactionCreate" in transactionCreateResultData + ? transactionCreateResultData.transactionCreate + : transactionCreateResultData; + + if ( + !transactionCreatePayload?.transaction || + transactionCreatePayload.errors.length > 0 + ) { + const mappedErrors = transactionCreatePayload?.errors.map( + (error) => ({ + code: error.code, + message: error.message ?? "Unknown transactionCreate error.", + }), + ); - return { - transaction: null, - errors: - mappedErrors && mappedErrors.length > 0 - ? mappedErrors - : [ - { - code: "UNKNOWN_TRANSACTION_CREATE_ERROR", - message: - "transactionCreate returned no transaction and no error details.", - }, - ], - }; - } + return { + transaction: null, + errors: + mappedErrors && mappedErrors.length > 0 + ? mappedErrors + : [ + { + code: "UNKNOWN_TRANSACTION_CREATE_ERROR", + message: + "transactionCreate returned no transaction and no error details.", + }, + ], + orderId: null, + }; + } - let checkoutCompleteErrors: ServiceError[]; + let checkoutCompleteOutcome: Awaited< + ReturnType + >; - try { - checkoutCompleteErrors = await completeCheckout(checkoutId); - } catch (error) { - checkoutCompleteErrors = mapCheckoutCompleteRequestFailure(error); - } + try { + checkoutCompleteOutcome = await completeCheckout(checkoutId); + } catch (error) { + return { + transaction: null, + errors: mapCheckoutCompleteRequestFailure(error), + orderId: null, + }; + } + + if (checkoutCompleteOutcome.errors.length > 0) { + return { + transaction: null, + errors: checkoutCompleteOutcome.errors, + orderId: null, + }; + } - if (checkoutCompleteErrors.length > 0) { return { - transaction: null, - errors: checkoutCompleteErrors, + transaction: transactionCreatePayload.transaction, + errors: [], + orderId: checkoutCompleteOutcome.orderId, }; - } - - return { - transaction: transactionCreatePayload.transaction, - errors: [], - }; - }), + }, + ), ); const failedTransactionCreates: FailedTransactionCreate[] = []; let createdTransactionsCount = 0; + const orderIdsForStripeCharge: string[] = []; settled.forEach((entry, index) => { const checkoutId = checkoutIds[index]; @@ -534,9 +575,31 @@ export async function POST(request: NextRequest) { return; } + if (entry.value.orderId) { + orderIdsForStripeCharge.push(entry.value.orderId); + } createdTransactionsCount += 1; }); + if (orderIdsForStripeCharge.length > 0) { + try { + await linkOrdersToStripeChargeFromPaymentIntent({ + authToken: config.authToken, + orderIds: orderIdsForStripeCharge, + paymentIntentId, + }); + } catch (error) { + marketplaceLogger.error( + "[stripe] linkOrdersToStripeChargeFromPaymentIntent failed", + { + paymentIntentId, + saleorDomain, + error: error instanceof Error ? error.message : String(error), + }, + ); + } + } + const hasAtLeastOneCreated = createdTransactionsCount > 0; const hasFailures = failedTransactionCreates.length > 0; diff --git a/apps/marketplace/src/app/api/payouts/batches/[id]/execute/route.ts b/apps/marketplace/src/app/api/payouts/batches/[id]/execute/route.ts new file mode 100644 index 00000000..cab9c87d --- /dev/null +++ b/apps/marketplace/src/app/api/payouts/batches/[id]/execute/route.ts @@ -0,0 +1,32 @@ +import { NextResponse } from "next/server"; + +import { getApiRouteAuthToken } from "@/lib/auth/server"; +import { executePayoutBatchTransfers } from "@/lib/ledger/execute-payout-batch"; + +export async function POST( + _request: Request, + context: { params: Promise<{ id: string }> }, +) { + const token = await getApiRouteAuthToken(); + + if (!token) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = await context.params; + + try { + const result = await executePayoutBatchTransfers(id); + + return NextResponse.json(result); + } catch (error) { + console.error("[payouts/execute]", error); + + return NextResponse.json( + { + error: error instanceof Error ? error.message : "Execute failed", + }, + { status: 500 }, + ); + } +} diff --git a/apps/marketplace/src/app/api/payouts/batches/[id]/route.ts b/apps/marketplace/src/app/api/payouts/batches/[id]/route.ts new file mode 100644 index 00000000..1a81cf42 --- /dev/null +++ b/apps/marketplace/src/app/api/payouts/batches/[id]/route.ts @@ -0,0 +1,59 @@ +import { NextResponse } from "next/server"; + +import { getApiRouteAuthToken } from "@/lib/auth/server"; +import { config } from "@/lib/config"; +import { getLedgerPool } from "@/lib/ledger/pool"; +import { getPayoutBatchWithItems } from "@/lib/ledger/repository"; +import { fetchVendorTitlesByIds } from "@/lib/saleor/fetch-vendor-page-titles"; + +export async function GET( + _request: Request, + context: { params: Promise<{ id: string }> }, +) { + const token = await getApiRouteAuthToken(); + + if (!token) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = await context.params; + const pool = getLedgerPool(); + + if (!pool) { + return NextResponse.json( + { error: "Ledger database not configured" }, + { status: 503 }, + ); + } + + const data = await getPayoutBatchWithItems(pool, id); + + if (!data.batch) { + return NextResponse.json({ error: "Not found" }, { status: 404 }); + } + + const vendorIdList = [...new Set(data.items.map((i) => i.vendor_id))]; + const vendorTitleById = await fetchVendorTitlesByIds(vendorIdList, token); + + const batch = { + ...data.batch, + created_at: data.batch.created_at.toISOString(), + executed_at: data.batch.executed_at + ? data.batch.executed_at.toISOString() + : null, + }; + + const items = data.items.map((item) => ({ + ...item, + stripe_transfer_created_at: item.stripe_transfer_created_at + ? item.stripe_transfer_created_at.toISOString() + : null, + })); + + return NextResponse.json({ + batch, + items, + saleorDashboardBaseUrl: config.saleor.dashboardBaseUrl, + vendorTitleById, + }); +} diff --git a/apps/marketplace/src/app/api/payouts/batches/close/route.ts b/apps/marketplace/src/app/api/payouts/batches/close/route.ts new file mode 100644 index 00000000..2fef3679 --- /dev/null +++ b/apps/marketplace/src/app/api/payouts/batches/close/route.ts @@ -0,0 +1,73 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; + +import { getApiRouteAuthToken } from "@/lib/auth/server"; +import { closePayoutBatchForPeriod } from "@/lib/ledger/close-payout-batch"; + +const bodySchema = z.object({ + createdBy: z.string().min(1).max(256).optional(), + currency: z.string().min(3).max(8), + periodEnd: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), + periodStart: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), +}); + +export async function POST(request: Request) { + const token = await getApiRouteAuthToken(); + + if (!token) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + } + + const parsed = bodySchema.safeParse(body); + + if (!parsed.success) { + return NextResponse.json( + { error: "Validation failed", issues: parsed.error.flatten() }, + { status: 400 }, + ); + } + + const result = await closePayoutBatchForPeriod({ + createdBy: parsed.data.createdBy ?? "marketplace", + currency: parsed.data.currency, + periodEnd: parsed.data.periodEnd, + periodStart: parsed.data.periodStart, + }); + + if (!result.ok) { + if (result.reason === "no_database") { + return NextResponse.json( + { error: "Ledger database not configured" }, + { status: 503 }, + ); + } + + if (result.reason === "no_eligible_lines") { + return NextResponse.json( + { + error: + "No eligible ledger lines for this period and currency (need order_gross with funds_status=available, not yet in a batch).", + }, + { status: 422 }, + ); + } + + return NextResponse.json( + { error: "Could not close period" }, + { status: 500 }, + ); + } + + return NextResponse.json({ + batchId: result.batchId, + itemCount: result.itemCount, + }); +} diff --git a/apps/marketplace/src/app/api/payouts/ledger/sync-stripe/route.ts b/apps/marketplace/src/app/api/payouts/ledger/sync-stripe/route.ts new file mode 100644 index 00000000..dde3a299 --- /dev/null +++ b/apps/marketplace/src/app/api/payouts/ledger/sync-stripe/route.ts @@ -0,0 +1,53 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; + +import { getApiRouteAuthToken } from "@/lib/auth/server"; +import { syncLedgerSettlementFromStripe } from "@/lib/ledger/sync-ledger-settlement-from-stripe"; + +const querySchema = z.object({ + chargeLimit: z.coerce.number().int().min(1).max(2000).optional(), +}); + +export async function POST(request: Request) { + const token = await getApiRouteAuthToken(); + + if (!token) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const url = new URL(request.url); + const parsedQuery = querySchema.safeParse({ + chargeLimit: url.searchParams.get("chargeLimit") ?? undefined, + }); + + if (!parsedQuery.success) { + return NextResponse.json( + { error: "Invalid query", issues: parsedQuery.error.flatten() }, + { status: 400 }, + ); + } + + try { + const result = await syncLedgerSettlementFromStripe({ + chargeLimit: parsedQuery.data.chargeLimit, + }); + + if (!result.ok) { + return NextResponse.json( + { error: "Ledger database not configured" }, + { status: 503 }, + ); + } + + return NextResponse.json({ + chargeErrors: result.chargeErrors, + chargesSynced: result.chargesSynced, + chargeIdsAttempted: result.chargeIdsAttempted, + promotedByDateCount: result.promotedByDateCount, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Sync failed"; + + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/apps/marketplace/src/app/api/payouts/overview/route.ts b/apps/marketplace/src/app/api/payouts/overview/route.ts new file mode 100644 index 00000000..58498292 --- /dev/null +++ b/apps/marketplace/src/app/api/payouts/overview/route.ts @@ -0,0 +1,114 @@ +import { NextResponse } from "next/server"; + +import { + getApiRouteAuthToken, + getVendorIdFromAccessToken, +} from "@/lib/auth/server"; +import { config } from "@/lib/config"; +import { getLedgerPool } from "@/lib/ledger/pool"; +import { + getVendorLedgerSummary, + listAllLedgerLines, + listRecentPayoutBatches, + listVendorLedgerLines, +} from "@/lib/ledger/repository"; +import { fetchVendorTitlesByIds } from "@/lib/saleor/fetch-vendor-page-titles"; + +const LEDGER_LINES_LIMIT = 200; + +export async function GET() { + const token = await getApiRouteAuthToken(); + + if (!token) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const pool = getLedgerPool(); + + if (!pool) { + return NextResponse.json({ + batches: [], + configured: false, + ledgerLines: [], + vendorSummary: null, + }); + } + + const vendorIdPromise = getVendorIdFromAccessToken(token); + + const [batches, vendorId] = await Promise.all([ + listRecentPayoutBatches(pool, 20), + vendorIdPromise, + ]); + + let ledgerLinesRaw: Awaited>; + let vendorSummaryRaw: Awaited< + ReturnType + > | null; + + if (vendorId) { + const [summary, lines] = await Promise.all([ + getVendorLedgerSummary(pool, vendorId), + listVendorLedgerLines(pool, vendorId, { + limit: LEDGER_LINES_LIMIT, + }), + ]); + + vendorSummaryRaw = summary; + ledgerLinesRaw = lines; + } else { + vendorSummaryRaw = null; + ledgerLinesRaw = await listAllLedgerLines(pool, { + limit: LEDGER_LINES_LIMIT, + }); + } + + const vendorSummary = vendorSummaryRaw + ? { + available_minor: vendorSummaryRaw.available_minor.toString(), + currency: vendorSummaryRaw.currency, + pending_minor: vendorSummaryRaw.pending_minor.toString(), + } + : null; + + const ledgerLines = ledgerLinesRaw.map((r) => ({ + amount_minor: r.amount_minor, + available_on: r.available_on ? r.available_on.toISOString() : null, + consumed_in_batch_id: r.consumed_in_batch_id, + currency: r.currency, + funds_status: r.funds_status, + id: r.id, + occurred_at: r.occurred_at.toISOString(), + order_id: r.order_id, + stripe_charge_id: r.stripe_charge_id, + vendor_id: r.vendor_id, + })); + + const vendorIdsForTitles = [...new Set(ledgerLines.map((l) => l.vendor_id))]; + const vendorTitleById = await fetchVendorTitlesByIds( + vendorIdsForTitles, + token, + ); + + const batchesOut = batches.map((b) => ({ + created_at: b.created_at.toISOString(), + currency: b.currency, + executed_at: b.executed_at ? b.executed_at.toISOString() : null, + id: b.id, + period_end: b.period_end, + period_start: b.period_start, + status: b.status, + transfer_initiated_at: b.transfer_initiated_at + ? b.transfer_initiated_at.toISOString() + : null, + })); + + return NextResponse.json({ + batches: batchesOut, + configured: true, + ledgerLines, + saleorDashboardBaseUrl: config.saleor.dashboardBaseUrl, + vendorSummary, + vendorTitleById, + }); +} diff --git a/apps/marketplace/src/app/api/saleor/manifest/route.ts b/apps/marketplace/src/app/api/saleor/manifest/route.ts index 24d8d10d..7ea33c26 100644 --- a/apps/marketplace/src/app/api/saleor/manifest/route.ts +++ b/apps/marketplace/src/app/api/saleor/manifest/route.ts @@ -107,6 +107,31 @@ export async function GET(request: NextRequest) { } } } +}`, + syncEvents: [], + }, + { + name: "Order paid (ledger ingest)", + targetUrl: manifestUrl(baseUrl, "/api/saleor/webhooks/order-paid"), + asyncEvents: ["ORDER_PAID"], + query: `subscription OrderPaidLedgerSubscription { + event { + ... on OrderPaid { + order { + id + metadata { + key + value + } + total { + gross { + amount + currency + } + } + } + } + } }`, syncEvents: [], }, diff --git a/apps/marketplace/src/app/api/saleor/webhooks/order-paid/route.ts b/apps/marketplace/src/app/api/saleor/webhooks/order-paid/route.ts new file mode 100644 index 00000000..769a9192 --- /dev/null +++ b/apps/marketplace/src/app/api/saleor/webhooks/order-paid/route.ts @@ -0,0 +1,345 @@ +import { type NextRequest, NextResponse } from "next/server"; + +import { MetadataUpdateDocument } from "@/graphql/generated/client"; +import { executeGraphQL } from "@/lib/graphql/execute"; +import { ingestOrderPaidToLedger } from "@/lib/ledger/ingest-order-paid"; +import { getChargeIdFromPaymentIntentId } from "@/lib/ledger/link-orders-stripe-charge"; +import { getLedgerPool } from "@/lib/ledger/pool"; +import { updateLedgerStripeChargeForOrders } from "@/lib/ledger/repository"; +import { applySettlementForCharge } from "@/lib/ledger/sync-ledger-settlement-from-stripe"; +import { getAppConfig } from "@/lib/saleor/app-config"; +import { METADATA_KEYS } from "@/lib/saleor/consts"; +import { + fetchOrderSnapshotForLedger, + type OrderLedgerSnapshot, + pickPaymentIntentIdFromOrderSnapshot, + resolveVendorIdFromOrderSnapshot, +} from "@/lib/saleor/fetch-order-for-ledger"; +import { marketplaceLogger } from "@/services/logging"; + +type OrderShape = { + id: string; + metadata?: Array<{ key: string; value: string }>; + total?: { + gross?: { amount?: number | string; currency?: string }; + }; +}; + +function extractOrderFromWebhookBody(body: unknown): OrderShape | null { + if (!body || typeof body !== "object") { + return null; + } + + const root = body as Record; + + if (root.order && typeof root.order === "object" && "id" in root.order) { + return root.order as OrderShape; + } + + if ( + root.__typename === "OrderPaid" && + root.order && + typeof root.order === "object" + ) { + return root.order as OrderShape; + } + + const topLevelEvent = root.event; + + if (topLevelEvent && typeof topLevelEvent === "object") { + const ev = topLevelEvent as Record; + + if (ev.order && typeof ev.order === "object" && "id" in ev.order) { + return ev.order as OrderShape; + } + } + + const data = root.data; + + if (data && typeof data === "object") { + const d = data as Record; + const event = d.event; + + if (event && typeof event === "object") { + const ev = event as Record; + + if (ev.order && typeof ev.order === "object" && "id" in ev.order) { + return ev.order as OrderShape; + } + } + + const orderPaid = d.orderPaid; + + if (orderPaid && typeof orderPaid === "object") { + const op = orderPaid as Record; + + if (op.order && typeof op.order === "object" && "id" in op.order) { + return op.order as OrderShape; + } + } + } + + return null; +} + +function findMetadata( + metadata: Array<{ key: string; value: string }> | undefined, + key: string, +): string | null { + const item = metadata?.find((m) => m.key === key); + + return item?.value?.trim() || null; +} + +function webhookOrderToSnapshot(order: OrderShape): OrderLedgerSnapshot { + const gross = order.total?.gross; + + return { + lines: [], + metadata: order.metadata ?? [], + total: + gross?.currency != null && + gross.amount !== undefined && + gross.amount !== null + ? { + gross: { + amount: + typeof gross.amount === "number" + ? gross.amount + : Number.parseFloat(String(gross.amount)), + currency: String(gross.currency), + }, + } + : null, + }; +} + +export async function POST(request: NextRequest) { + try { + const saleorDomain = request.headers.get("saleor-domain"); + + if (!saleorDomain) { + return NextResponse.json( + { error: "Missing saleor-domain header" }, + { status: 400 }, + ); + } + + const appConfig = await getAppConfig(saleorDomain); + + if (!appConfig) { + return NextResponse.json( + { error: "App not configured for this domain" }, + { status: 500 }, + ); + } + + const bodyUnknown: unknown = await request.json(); + const orderFromWebhook = extractOrderFromWebhookBody(bodyUnknown); + + if (!orderFromWebhook?.id) { + marketplaceLogger.warning( + "[order-paid] skipped: could not parse order from payload", + { + bodyKeys: + bodyUnknown && typeof bodyUnknown === "object" + ? Object.keys(bodyUnknown) + : [], + }, + ); + + return NextResponse.json({ status: "skipped", reason: "no_order" }); + } + + const snapshotFromSaleor = await fetchOrderSnapshotForLedger({ + authToken: appConfig.authToken, + orderId: orderFromWebhook.id, + saleorDomain, + }); + + const webhookSnapshot = webhookOrderToSnapshot(orderFromWebhook); + const snapshot: OrderLedgerSnapshot = snapshotFromSaleor ?? webhookSnapshot; + + const occurredAtForLedger = + snapshot.created != null + ? (() => { + const d = new Date(snapshot.created); + + return Number.isNaN(d.getTime()) ? new Date() : d; + })() + : new Date(); + + const vendorId = + resolveVendorIdFromOrderSnapshot(snapshot) ?? + resolveVendorIdFromOrderSnapshot(webhookSnapshot); + + if (!vendorId) { + marketplaceLogger.warning( + "[order-paid] skipped: no vendor on order or lines", + { + orderId: orderFromWebhook.id, + usedSaleorFetch: snapshotFromSaleor != null, + }, + ); + + return NextResponse.json({ + status: "skipped", + reason: "no_vendor_in_order_metadata", + orderId: orderFromWebhook.id, + }); + } + + const gross = + snapshot.total?.gross ?? + webhookSnapshot.total?.gross ?? + (() => { + const g = orderFromWebhook.total?.gross; + + if (!g?.currency || g.amount === undefined || g.amount === null) { + return null; + } + + return { + amount: + typeof g.amount === "number" + ? g.amount + : Number.parseFloat(String(g.amount)), + currency: String(g.currency), + }; + })(); + + if (!gross?.currency || !Number.isFinite(gross.amount)) { + marketplaceLogger.warning("[order-paid] skipped: missing total gross", { + orderId: orderFromWebhook.id, + }); + + return NextResponse.json({ + status: "skipped", + reason: "no_total_gross", + orderId: orderFromWebhook.id, + }); + } + + const metadataForStripe = + snapshot.metadata.length > 0 + ? snapshot.metadata + : (orderFromWebhook.metadata ?? []); + + let stripeChargeId = + findMetadata(metadataForStripe, METADATA_KEYS.STRIPE_CHARGE_ID) ?? + findMetadata(metadataForStripe, "stripe_charge_id"); + + if (!stripeChargeId && snapshotFromSaleor) { + const paymentIntentId = + pickPaymentIntentIdFromOrderSnapshot(snapshotFromSaleor); + + if (paymentIntentId) { + try { + const resolved = + await getChargeIdFromPaymentIntentId(paymentIntentId); + + if (resolved) { + stripeChargeId = resolved; + const metaResult = await executeGraphQL( + MetadataUpdateDocument, + "MetadataUpdateMutation", + { + id: orderFromWebhook.id, + input: [ + { + key: METADATA_KEYS.STRIPE_CHARGE_ID, + value: resolved, + }, + ], + }, + appConfig.authToken, + ); + + if (!metaResult.ok) { + marketplaceLogger.warning( + "[order-paid] stripe_charge_id metadata backfill failed", + { + errors: metaResult.errors, + orderId: orderFromWebhook.id, + }, + ); + } + } + } catch (error) { + marketplaceLogger.warning( + "[order-paid] could not resolve Stripe charge from PaymentIntent", + { + error: error instanceof Error ? error.message : String(error), + orderId: orderFromWebhook.id, + paymentIntentId, + }, + ); + } + } + } + + const ledger = await ingestOrderPaidToLedger({ + currency: gross.currency, + grossAmount: gross.amount, + occurredAt: occurredAtForLedger, + orderId: orderFromWebhook.id, + stripeChargeId, + vendorId, + }); + + const pool = getLedgerPool(); + + if (stripeChargeId && pool) { + await updateLedgerStripeChargeForOrders(pool, { + chargeId: stripeChargeId, + orderIds: [orderFromWebhook.id], + }); + + try { + await applySettlementForCharge(stripeChargeId); + } catch (error) { + marketplaceLogger.warning( + "[order-paid] Stripe settlement sync failed after charge", + { + chargeId: stripeChargeId, + error: error instanceof Error ? error.message : String(error), + orderId: orderFromWebhook.id, + }, + ); + } + } + + if (ledger.status !== "recorded") { + marketplaceLogger.warning("[order-paid] ledger ingest skipped", { + orderId: orderFromWebhook.id, + reason: ledger.reason, + vendorId, + }); + } else { + marketplaceLogger.info("[order-paid] ledger entry recorded", { + currency: gross.currency, + grossAmount: gross.amount, + orderId: orderFromWebhook.id, + vendorId, + }); + } + + return NextResponse.json({ + ledger, + orderId: orderFromWebhook.id, + reason: ledger.reason, + status: ledger.status === "recorded" ? "success" : "skipped", + vendorId, + }); + } catch (error) { + console.error("[order-paid] webhook failed", error); + + return NextResponse.json( + { + status: "error", + message: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 }, + ); + } +} diff --git a/apps/marketplace/src/app/api/stripe/connect/webhook/route.ts b/apps/marketplace/src/app/api/stripe/connect/webhook/route.ts index 90d98bd9..4598b712 100644 --- a/apps/marketplace/src/app/api/stripe/connect/webhook/route.ts +++ b/apps/marketplace/src/app/api/stripe/connect/webhook/route.ts @@ -1,5 +1,7 @@ import { NextResponse } from "next/server"; +import { getLedgerPool } from "@/lib/ledger/pool"; +import { upsertVendorStripeAccount } from "@/lib/ledger/repository"; import { METADATA_KEYS } from "@/lib/saleor/consts"; import { getVendorPageMetadata, @@ -10,20 +12,30 @@ import { isStripeConnectOnboardingCompleted, verifyStripeWebhookSignature, } from "@/lib/stripe/connect"; - -type StripeAccountUpdatedEvent = { - data?: { - object?: { - details_submitted?: boolean; - id?: string; - metadata?: Record; - requirements?: { - currently_due?: string[]; - }; - }; - }; +import { + finalizeStripeWebhookInbox, + isLedgerStripeEventType, + processLedgerStripeSideEffects, + type StripeWebhookEventEnvelope, + tryRecordStripeWebhookInbox, +} from "@/lib/stripe/process-ledger-stripe-webhook"; + +type StripeAccountObject = { + default_currency?: string; + details_submitted?: boolean; id?: string; - type?: string; + metadata?: Record; + payouts_enabled?: boolean; + requirements?: { + currently_due?: string[]; + }; +}; + +type StripeIncomingEvent = { + data?: { object?: StripeAccountObject }; + id: string; + livemode: boolean; + type: string; }; function resolveDefaultSaleorDomain(): string | null { @@ -61,71 +73,142 @@ export async function POST(request: Request) { ); } - const event = JSON.parse(payload) as StripeAccountUpdatedEvent; + const event = JSON.parse(payload) as StripeIncomingEvent; + + if (event.type === "account.updated") { + const account = event.data?.object; + + if (!account?.id) { + return NextResponse.json( + { error: "Missing account id in Stripe event" }, + { status: 400 }, + ); + } + + const vendorPageId = account.metadata?.vendor_id?.trim(); + + if (!vendorPageId) { + return NextResponse.json({ + status: "skipped", + reason: "missing_vendor_id", + }); + } + + const saleorDomainFromMetadata = account.metadata?.saleor_domain?.trim(); + const saleorDomain = + saleorDomainFromMetadata || resolveDefaultSaleorDomain(); + + if (!saleorDomain) { + return NextResponse.json( + { error: "Cannot resolve Saleor domain for webhook update" }, + { status: 500 }, + ); + } + + const connected = isStripeConnectOnboardingCompleted({ + details_submitted: account.details_submitted, + requirements: account.requirements, + }); + const currentMetadata = await getVendorPageMetadata({ + saleorDomain, + vendorPageId, + }); - if (event.type !== "account.updated") { - return NextResponse.json({ status: "ignored", type: event.type ?? null }); - } + await updateVendorPageMetadata({ + saleorDomain, + vendorPageId, + metadata: mergeMetadata(currentMetadata, [ + { + key: METADATA_KEYS.PAYMENT_ACCOUNT_ID, + value: account.id, + }, + { + key: METADATA_KEYS.PAYMENT_ACCOUNT_CONNECTED, + value: connected ? "true" : "false", + }, + ]), + }); - const account = event.data?.object; + const pool = getLedgerPool(); - if (!account?.id) { - return NextResponse.json( - { error: "Missing account id in Stripe event" }, - { status: 400 }, - ); - } + if (pool) { + const defaultCurrency = + typeof account.default_currency === "string" + ? account.default_currency + : "usd"; - const vendorPageId = account.metadata?.vendor_id?.trim(); + await upsertVendorStripeAccount(pool, { + defaultCurrency, + onboardingCompleted: connected, + payoutsEnabled: Boolean(account.payouts_enabled), + stripeAccountId: account.id, + vendorId: vendorPageId, + }); + } - if (!vendorPageId) { return NextResponse.json({ - status: "skipped", - reason: "missing_vendor_id", + status: "processed", + connected, + stripeAccountId: account.id, + vendorPageId, }); } - const saleorDomainFromMetadata = account.metadata?.saleor_domain?.trim(); - const saleorDomain = - saleorDomainFromMetadata || resolveDefaultSaleorDomain(); + const pool = getLedgerPool(); - if (!saleorDomain) { - return NextResponse.json( - { error: "Cannot resolve Saleor domain for webhook update" }, - { status: 500 }, + if (pool && isLedgerStripeEventType(event.type)) { + const envelope: StripeWebhookEventEnvelope = { + data: event.data, + id: event.id, + livemode: event.livemode, + type: event.type, + }; + + const inserted = await tryRecordStripeWebhookInbox( + pool, + payload, + envelope, ); + + if (!inserted) { + return NextResponse.json({ + eventId: event.id, + status: "duplicate", + }); + } + + try { + await processLedgerStripeSideEffects(pool, envelope); + await finalizeStripeWebhookInbox(pool, event.id, "ok"); + } catch (error) { + await finalizeStripeWebhookInbox( + pool, + event.id, + error instanceof Error ? error.message : "error", + ); + console.error( + "[stripe-connect] Ledger webhook processing failed", + error, + ); + + return NextResponse.json( + { + error: + error instanceof Error + ? error.message + : "Ledger webhook processing failed", + }, + { status: 500 }, + ); + } + + return NextResponse.json({ + ledger: true, + status: "processed", + }); } - const connected = isStripeConnectOnboardingCompleted({ - details_submitted: account.details_submitted, - requirements: account.requirements, - }); - const currentMetadata = await getVendorPageMetadata({ - saleorDomain, - vendorPageId, - }); - - await updateVendorPageMetadata({ - saleorDomain, - vendorPageId, - metadata: mergeMetadata(currentMetadata, [ - { - key: METADATA_KEYS.PAYMENT_ACCOUNT_ID, - value: account.id, - }, - { - key: METADATA_KEYS.PAYMENT_ACCOUNT_CONNECTED, - value: connected ? "true" : "false", - }, - ]), - }); - - return NextResponse.json({ - status: "processed", - connected, - vendorPageId, - stripeAccountId: account.id, - }); + return NextResponse.json({ status: "ignored", type: event.type }); } catch (error) { console.error("[stripe-connect] Failed to process webhook", error); diff --git a/apps/marketplace/src/app/app/_components/app-page-client.tsx b/apps/marketplace/src/app/app/_components/app-page-client.tsx index ee89e958..e21fabd6 100644 --- a/apps/marketplace/src/app/app/_components/app-page-client.tsx +++ b/apps/marketplace/src/app/app/_components/app-page-client.tsx @@ -5,8 +5,10 @@ import { Settings, ShoppingCart, Users, + Wallet, Warehouse, } from "lucide-react"; +import { useTranslations } from "next-intl"; import { Card, @@ -23,18 +25,23 @@ import { } from "@nimara/ui/components/tabs"; import { AppOptionsTab } from "@/app/app/_components/app-options-tab"; +import { AppPayoutsOverviewTab } from "@/app/app/_components/app-payouts-overview-tab"; import { AppVendorsTab } from "@/app/app/_components/app-vendors-tab"; import { APP_CONFIG } from "@/lib/saleor/consts"; import { useAuth } from "@/providers/auth-provider"; export function AppPageClient() { const { isAuthenticated, isLoading } = useAuth(); + const t = useTranslations(); return (
- + Vendors + + {t("marketplace.payouts.tab-label")} + Options About @@ -101,6 +108,25 @@ export function AppPageClient() { + + + + + + {t("marketplace.payouts.title")} + + + {t("marketplace.payouts.description")} + + + + + + +
); diff --git a/apps/marketplace/src/app/app/_components/app-payouts-overview-tab.tsx b/apps/marketplace/src/app/app/_components/app-payouts-overview-tab.tsx new file mode 100644 index 00000000..3b043656 --- /dev/null +++ b/apps/marketplace/src/app/app/_components/app-payouts-overview-tab.tsx @@ -0,0 +1,1123 @@ +"use client"; + +import { ChevronDown, ChevronRight } from "lucide-react"; +import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { Fragment, useCallback, useEffect, useState } from "react"; + +import { Button } from "@nimara/ui/components/button"; +import { Input } from "@nimara/ui/components/input"; +import { Label } from "@nimara/ui/components/label"; +import { Separator } from "@nimara/ui/components/separator"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { cn, formatDateTime } from "@/lib/utils"; +import { useAuth } from "@/providers/auth-provider"; + +type Props = { + isAuthenticated: boolean; + isLoading: boolean; +}; + +type PayoutBatchRow = { + created_at: string; + currency: string; + executed_at: string | null; + id: string; + period_end: string; + period_start: string; + status: string; + transfer_initiated_at: string | null; +}; + +type LedgerLineRow = { + amount_minor: string; + available_on: string | null; + consumed_in_batch_id: string | null; + currency: string; + funds_status: string; + id: string; + occurred_at: string; + order_id: string | null; + stripe_charge_id: string | null; + vendor_id: string; +}; + +type OverviewSuccess = { + batches: PayoutBatchRow[]; + configured: true; + ledgerLines: LedgerLineRow[]; + saleorDashboardBaseUrl: string | null; + vendorSummary: { + available_minor: string; + currency: string | null; + pending_minor: string; + } | null; + vendorTitleById: Record; +}; + +type OverviewNotConfigured = { + batches: []; + configured: false; + ledgerLines: []; + vendorSummary: null; +}; + +type OverviewResponse = OverviewNotConfigured | OverviewSuccess; + +type BatchDetailResponse = { + batch: { + created_at: string; + currency: string; + executed_at: string | null; + id: string; + period_end: string; + period_start: string; + status: string; + } | null; + items: Array<{ + fees_minor: string; + gross_minor: string; + id: string; + net_minor: string; + status: string; + stripe_transfer_created_at: string | null; + vendor_id: string; + }>; + saleorDashboardBaseUrl: string | null; + vendorTitleById: Record; +}; + +function formatMinorAmount(amountMinorStr: string, currency: string): string { + try { + const minor = BigInt(amountMinorStr); + const major = Number(minor) / 100; + + return new Intl.NumberFormat(undefined, { + currency: currency.toUpperCase(), + style: "currency", + }).format(major); + } catch { + return amountMinorStr; + } +} + +function fundsStatusClassName(status: string): string { + switch (status) { + case "available": + return "text-green-600"; + case "held": + return "text-orange-600"; + case "pending_stripe": + return "text-amber-600"; + case "refunded": + case "reversed": + return "text-muted-foreground"; + default: + return ""; + } +} + +function formatUtcDateTime(iso: string): string { + return new Intl.DateTimeFormat(undefined, { + dateStyle: "medium", + timeStyle: "short", + timeZone: "UTC", + }).format(new Date(iso)); +} + +/** `period_*` from API are YYYY-MM-DD (no timezone shift). */ +function formatPeriodRange(periodStart: string, periodEnd: string): string { + const parseYmd = (s: string) => { + const [y, m, d] = s.split("-").map((x) => Number.parseInt(x, 10)); + + return new Date(y, m - 1, d); + }; + + const fmt = new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }); + + return `${fmt.format(parseYmd(periodStart))} – ${fmt.format(parseYmd(periodEnd))}`; +} + +function dashboardModelHref( + base: string | null, + vendorId: string, +): string | null { + if (!base) { + return null; + } + + return `${base}/models/${encodeURIComponent(vendorId)}`; +} + +function dashboardOrderHref( + base: string | null, + orderId: string | null, +): string | null { + if (!base || !orderId) { + return null; + } + + return `${base}/orders/${encodeURIComponent(orderId)}`; +} + +export function AppPayoutsOverviewTab({ isAuthenticated, isLoading }: Props) { + const t = useTranslations(); + const { apiAccessToken, dashboardContext } = useAuth(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [overview, setOverview] = useState(null); + const [expandedId, setExpandedId] = useState(null); + const [batchDetail, setBatchDetail] = useState< + Record + >({}); + const [periodStart, setPeriodStart] = useState(""); + const [periodEnd, setPeriodEnd] = useState(""); + const [closeCurrency, setCloseCurrency] = useState("usd"); + const [closing, setClosing] = useState(false); + const [closeMessage, setCloseMessage] = useState<{ + text: string; + type: "error" | "success"; + } | null>(null); + const [executingBatchId, setExecutingBatchId] = useState(null); + const [executeMessage, setExecuteMessage] = useState<{ + batchId: string; + text: string; + type: "error" | "success"; + } | null>(null); + const [syncingStripe, setSyncingStripe] = useState(false); + const [syncStripeMessage, setSyncStripeMessage] = useState<{ + text: string; + type: "error" | "success" | "warning"; + } | null>(null); + + const fetchOverview = useCallback(async () => { + if (!isAuthenticated) { + return; + } + + if (!apiAccessToken) { + return; + } + + setLoading(true); + setError(null); + + try { + const res = await fetch("/api/payouts/overview", { + credentials: "include", + headers: { + Authorization: `Bearer ${apiAccessToken}`, + }, + }); + + if (res.status === 401) { + setError(t("marketplace.payouts.unauthorized")); + + return; + } + + if (!res.ok) { + setError(t("marketplace.payouts.failed-to-load")); + + return; + } + + const data = (await res.json()) as OverviewResponse; + + setOverview(data); + } catch { + setError(t("marketplace.payouts.failed-to-load")); + } finally { + setLoading(false); + } + }, [apiAccessToken, isAuthenticated, t]); + + useEffect(() => { + void fetchOverview(); + }, [fetchOverview]); + + useEffect(() => { + const d = new Date(); + const start = new Date(d.getFullYear(), d.getMonth(), 1); + const end = new Date(d.getFullYear(), d.getMonth() + 1, 0); + const fmt = (x: Date) => x.toISOString().slice(0, 10); + + setPeriodStart(fmt(start)); + setPeriodEnd(fmt(end)); + }, []); + + const loadBatchDetail = useCallback( + async (batchId: string) => { + if (!apiAccessToken) { + return; + } + + let shouldFetch = true; + + setBatchDetail((prev) => { + const existing = prev[batchId]; + + if ( + existing && + existing !== "error" && + existing !== "loading" && + typeof existing === "object" + ) { + shouldFetch = false; + + return prev; + } + + return { ...prev, [batchId]: "loading" }; + }); + + if (!shouldFetch) { + return; + } + + try { + const res = await fetch(`/api/payouts/batches/${batchId}`, { + credentials: "include", + headers: { + Authorization: `Bearer ${apiAccessToken}`, + }, + }); + + if (!res.ok) { + setBatchDetail((prev) => ({ ...prev, [batchId]: "error" })); + + return; + } + + const data = (await res.json()) as BatchDetailResponse; + + setBatchDetail((prev) => ({ ...prev, [batchId]: data })); + } catch { + setBatchDetail((prev) => ({ ...prev, [batchId]: "error" })); + } + }, + [apiAccessToken], + ); + + const handleClosePeriod = async () => { + if (!apiAccessToken) { + return; + } + + setClosing(true); + setCloseMessage(null); + + try { + const res = await fetch("/api/payouts/batches/close", { + body: JSON.stringify({ + createdBy: "saleor-dashboard", + currency: closeCurrency.trim(), + periodEnd, + periodStart, + }), + credentials: "include", + headers: { + Authorization: `Bearer ${apiAccessToken}`, + "Content-Type": "application/json", + }, + method: "POST", + }); + + if (res.status === 422) { + setCloseMessage({ + text: t("marketplace.payouts.close-period.error-422"), + type: "error", + }); + + return; + } + + if (!res.ok) { + setCloseMessage({ + text: t("marketplace.payouts.close-period.error"), + type: "error", + }); + + return; + } + + setCloseMessage({ + text: t("marketplace.payouts.close-period.success"), + type: "success", + }); + await fetchOverview(); + } catch { + setCloseMessage({ + text: t("marketplace.payouts.close-period.error"), + type: "error", + }); + } finally { + setClosing(false); + } + }; + + const handleSyncStripe = async () => { + if (!apiAccessToken) { + return; + } + + setSyncingStripe(true); + setSyncStripeMessage(null); + + try { + const res = await fetch("/api/payouts/ledger/sync-stripe", { + credentials: "include", + headers: { + Authorization: `Bearer ${apiAccessToken}`, + }, + method: "POST", + }); + + if (!res.ok) { + const body = (await res.json().catch(() => null)) as { + error?: string; + } | null; + + setSyncStripeMessage({ + text: body?.error ?? t("marketplace.payouts.sync-stripe.error"), + type: "error", + }); + + return; + } + + const data = (await res.json()) as { + chargeErrors?: Array<{ chargeId: string; message: string }>; + chargeIdsAttempted: number; + chargesSynced: number; + promotedByDateCount: number; + }; + + const errCount = data.chargeErrors?.length ?? 0; + const base = t("marketplace.payouts.sync-stripe.success", { + charges: data.chargesSynced, + promoted: data.promotedByDateCount, + }); + const extra = + errCount > 0 + ? ` ${t("marketplace.payouts.sync-stripe.partial", { count: errCount })}` + : ""; + + setSyncStripeMessage({ + text: `${base}${extra}`, + type: errCount > 0 ? "warning" : "success", + }); + await fetchOverview(); + } catch { + setSyncStripeMessage({ + text: t("marketplace.payouts.sync-stripe.error"), + type: "error", + }); + } finally { + setSyncingStripe(false); + } + }; + + const handleExecuteBatch = async (batchId: string) => { + if (!apiAccessToken) { + return; + } + + setExecutingBatchId(batchId); + setExecuteMessage(null); + + try { + const res = await fetch(`/api/payouts/batches/${batchId}/execute`, { + credentials: "include", + headers: { + Authorization: `Bearer ${apiAccessToken}`, + }, + method: "POST", + }); + + const data = (await res.json().catch(() => null)) as { + batchStatus?: string; + error?: string; + processed?: number; + } | null; + + if (!res.ok) { + setExecuteMessage({ + batchId, + text: data?.error ?? t("marketplace.payouts.execute.error"), + type: "error", + }); + + return; + } + + setExecuteMessage({ + batchId, + text: `${t("marketplace.payouts.execute.success")} (${data?.processed ?? 0} transfers, status: ${data?.batchStatus ?? "—"})`, + type: "success", + }); + setBatchDetail((prev) => { + const next = { ...prev }; + + delete next[batchId]; + + return next; + }); + await fetchOverview(); + await loadBatchDetail(batchId); + } catch { + setExecuteMessage({ + batchId, + text: t("marketplace.payouts.execute.error"), + type: "error", + }); + } finally { + setExecutingBatchId(null); + } + }; + + const toggleExpand = async (batchId: string) => { + if (expandedId === batchId) { + setExpandedId(null); + + return; + } + + setExpandedId(batchId); + await loadBatchDetail(batchId); + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!isAuthenticated) { + return ( +
+

+ {t("marketplace.payouts.sign-in-to-view")} +

+ +
+ ); + } + + if (!dashboardContext) { + return ( +
+

+ {t("marketplace.payouts.open-from-dashboard")} +

+
+ ); + } + + if (isAuthenticated && !apiAccessToken) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+

{error}

+
+ ); + } + + if (loading || overview === null) { + return ( +
+
+
+ ); + } + + if (!overview.configured) { + return ( +
+

+ {t("marketplace.payouts.not-configured")} +

+
+ ); + } + + const vendorSummary = overview.vendorSummary; + const dashBase = overview.saleorDashboardBaseUrl; + const vendorTitleById = overview.vendorTitleById; + const pendingBatches = overview.batches.filter((b) => b.status !== "paid"); + const paidBatches = overview.batches.filter((b) => b.status === "paid"); + + const renderBatchRows = (rows: PayoutBatchRow[]) => + rows.map((row) => { + const isOpen = expandedId === row.id; + const detail = batchDetail[row.id]; + + return ( + + + + + + {row.id} + + {formatPeriodRange(row.period_start, row.period_end)} + + {row.currency.toUpperCase()} + + {row.status} + + + {formatDateTime(row.created_at)} + + + {row.transfer_initiated_at + ? formatUtcDateTime(row.transfer_initiated_at) + : "—"} + + + {isOpen ? ( + + +
+ {detail === "loading" || detail === undefined ? ( +
+
+
+ ) : detail === "error" ? ( +

+ {t("marketplace.payouts.failed-to-load-batch")} +

+ ) : !detail.batch ? ( +

+ {t("marketplace.payouts.failed-to-load-batch")} +

+ ) : ( +
+ {executeMessage && executeMessage.batchId === row.id ? ( +

+ {executeMessage.text} +

+ ) : null} + {detail.batch && + ["locked", "partially_paid", "executing"].includes( + detail.batch.status, + ) ? ( +
+ + + {t("marketplace.payouts.execute.hint")} + +
+ ) : null} +
+ + + + + {t("marketplace.payouts.ledgerLines.colVendor")} + + + {t("marketplace.payouts.batch.gross")} + + + {t("marketplace.payouts.batch.fees")} + + + {t("marketplace.payouts.batch.net")} + + + {t( + "marketplace.payouts.batch.transfer-initiated", + )} + + {t("common.status")} + + + + {detail.items.map((item) => { + const itemDash = + detail.saleorDashboardBaseUrl ?? dashBase; + const vName = + detail.vendorTitleById[item.vendor_id] ?? + vendorTitleById[item.vendor_id] ?? + item.vendor_id; + const vHref = dashboardModelHref( + itemDash, + item.vendor_id, + ); + + return ( + + + {vHref ? ( + + {vName} + + ) : ( + + {vName} + + )} + + + {formatMinorAmount( + item.gross_minor, + detail.batch!.currency, + )} + + + {formatMinorAmount( + item.fees_minor, + detail.batch!.currency, + )} + + + {formatMinorAmount( + item.net_minor, + detail.batch!.currency, + )} + + + {item.stripe_transfer_created_at + ? formatUtcDateTime( + item.stripe_transfer_created_at, + ) + : "—"} + + {item.status} + + ); + })} + +
+
+
+ )} +
+ + + ) : null} + + ); + }); + + return ( +
+
+
+

+ {t("marketplace.payouts.sync-stripe.title")} +

+

+ {t("marketplace.payouts.sync-stripe.description")} +

+
+ + {syncStripeMessage ? ( +

+ {syncStripeMessage.text} +

+ ) : null} +
+ +
+
+

+ {t("marketplace.payouts.ledgerLines.title")} +

+

+ {t("marketplace.payouts.ledgerLines.description")} +

+
+ {overview.ledgerLines.length === 0 ? ( +

+ {t("marketplace.payouts.ledgerLines.empty")} +

+ ) : ( +
+ + + + + {t("marketplace.payouts.ledgerLines.colVendor")} + + + {t("marketplace.payouts.ledgerLines.colOrder")} + + + {t("marketplace.payouts.ledgerLines.colAmount")} + + + {t("marketplace.payouts.ledgerLines.colStatus")} + + + {t("marketplace.payouts.ledgerLines.colAvailable")} + + + {t("marketplace.payouts.ledgerLines.colCharge")} + + + {t("marketplace.payouts.ledgerLines.colOrderPlaced")} + + + + + {overview.ledgerLines.map((line) => { + const vName = + vendorTitleById[line.vendor_id] ?? line.vendor_id; + const vHref = dashboardModelHref(dashBase, line.vendor_id); + const orderHref = dashboardOrderHref(dashBase, line.order_id); + + return ( + + + {vHref ? ( + + {vName} + + ) : ( + + {vName} + + )} + + + {line.order_id ? ( + orderHref ? ( + + {line.order_id} + + ) : ( + + {line.order_id} + + ) + ) : ( + "—" + )} + + + {formatMinorAmount(line.amount_minor, line.currency)} + + + {line.funds_status} + + + {line.available_on + ? formatUtcDateTime(line.available_on) + : t( + "marketplace.payouts.ledgerLines.availableUnknown", + )} + + + {line.stripe_charge_id ?? "—"} + + + {formatDateTime(line.occurred_at)} + + + ); + })} + +
+
+ )} +
+ +
+
+

+ {t("marketplace.payouts.close-period.title")} +

+

+ {t("marketplace.payouts.close-period.description")} +

+
+
+
+ + setPeriodStart(e.target.value)} + /> +
+
+ + setPeriodEnd(e.target.value)} + /> +
+
+ + setCloseCurrency(e.target.value)} + className="lowercase" + /> +
+
+
+ +
+ {closeMessage ? ( +

+ {closeMessage.text} +

+ ) : null} +
+ + {vendorSummary ? ( +
+

+ {t("marketplace.payouts.vendor-summary-title")} +

+
+ + {t("marketplace.payouts.vendor-available")}:{" "} + + {formatMinorAmount( + vendorSummary.available_minor, + vendorSummary.currency ?? "USD", + )} + + + + {t("marketplace.payouts.vendor-pending")}:{" "} + + {formatMinorAmount( + vendorSummary.pending_minor, + vendorSummary.currency ?? "USD", + )} + + +
+
+ ) : null} + + {overview.batches.length === 0 ? ( +
+

+ {t("marketplace.payouts.no-batches")} +

+
+ ) : ( +
+ {pendingBatches.length > 0 ? ( +
+

+ {t("marketplace.payouts.batches-pending-title")} +

+
+ + + + + {t("marketplace.payouts.table.id")} + + {t("marketplace.payouts.table.period")} + + + {t("marketplace.payouts.table.currency")} + + + {t("marketplace.payouts.table.status")} + + + {t("marketplace.payouts.table.created")} + + + {t("marketplace.payouts.table.transferInitiated")} + + + + {renderBatchRows(pendingBatches)} +
+
+
+ ) : null} + + {pendingBatches.length > 0 && paidBatches.length > 0 ? ( + + ) : null} + + {paidBatches.length > 0 ? ( +
+

+ {t("marketplace.payouts.batches-paid-title")} +

+
+ + + + + {t("marketplace.payouts.table.id")} + + {t("marketplace.payouts.table.period")} + + + {t("marketplace.payouts.table.currency")} + + + {t("marketplace.payouts.table.status")} + + + {t("marketplace.payouts.table.created")} + + + {t("marketplace.payouts.table.transferInitiated")} + + + + {renderBatchRows(paidBatches)} +
+
+
+ ) : null} +
+ )} +
+ ); +} diff --git a/apps/marketplace/src/lib/auth/server.ts b/apps/marketplace/src/lib/auth/server.ts index 3cfa9c1b..4eeda20f 100644 --- a/apps/marketplace/src/lib/auth/server.ts +++ b/apps/marketplace/src/lib/auth/server.ts @@ -1,5 +1,5 @@ import * as jose from "jose"; -import { cookies } from "next/headers"; +import { cookies, headers } from "next/headers"; import { getAppConfig } from "@/lib/saleor/app-config"; import { METADATA_KEYS } from "@/lib/saleor/consts"; @@ -15,17 +15,30 @@ export async function getServerAuthToken(): Promise { } /** - * Get vendor ID for the current server session. - * Used when Server Actions need vendor context (e.g. updateMetadata fallback). - * Requires: auth token in cookies, app config for Saleor domain, user metadata with vendor.id. + * Bearer header (client fetch from App Bridge / iframe) then cookie. + * Use in Route Handlers where `credentials: include` may not send cookies reliably. */ -export async function getServerVendorId(): Promise { - const token = await getServerAuthToken(); +export async function getApiRouteAuthToken(): Promise { + const headerList = await headers(); + const authorization = headerList.get("authorization"); - if (!token) { - return null; + if (authorization?.toLowerCase().startsWith("bearer ")) { + const value = authorization.slice(7).trim(); + + if (value) { + return value; + } } + return getServerAuthToken(); +} + +/** + * Resolve vendor profile id from a Saleor user JWT (vendor dashboard session or App Bridge). + */ +export async function getVendorIdFromAccessToken( + accessToken: string, +): Promise { let saleorDomain: string | null = null; const url = process.env.NEXT_PUBLIC_SALEOR_URL; @@ -39,10 +52,10 @@ export async function getServerVendorId(): Promise { if (!saleorDomain) { try { - const decoded = jose.decodeJwt(token) as { iss?: string }; + const decodedIss = jose.decodeJwt(accessToken) as { iss?: string }; - if (decoded.iss) { - saleorDomain = new URL(decoded.iss).host; + if (decodedIss.iss) { + saleorDomain = new URL(decodedIss.iss).host; } } catch { // ignore @@ -59,7 +72,7 @@ export async function getServerVendorId(): Promise { return null; } - const decoded = jose.decodeJwt(token) as { user_id?: string }; + const decoded = jose.decodeJwt(accessToken) as { user_id?: string }; const userId = decoded.user_id; if (!userId) { @@ -93,3 +106,18 @@ export async function getServerVendorId(): Promise { return vendorId || null; } + +/** + * Get vendor ID for the current server session. + * Used when Server Actions need vendor context (e.g. updateMetadata fallback). + * Requires: auth token in cookies, app config for Saleor domain, user metadata with vendor.id. + */ +export async function getServerVendorId(): Promise { + const token = await getServerAuthToken(); + + if (!token) { + return null; + } + + return getVendorIdFromAccessToken(token); +} diff --git a/apps/marketplace/src/lib/config.ts b/apps/marketplace/src/lib/config.ts index 9f771d11..c90f8e4b 100644 --- a/apps/marketplace/src/lib/config.ts +++ b/apps/marketplace/src/lib/config.ts @@ -86,6 +86,9 @@ const envSchema = z.object({ STRIPE_SECRET_KEY: z.string().optional(), MARKETPLACE_STRIPE_CONNECT_WEBHOOK_SECRET: z.string().optional(), MARKETPLACE_STRIPE_CONNECT_DEFAULT_COUNTRY: z.string().default("US"), + + /** Postgres URL for ledger + payout tables (see db/migrations/001_init_ledger.sql) */ + DATABASE_URL: z.string().optional(), }); export type Env = z.infer; @@ -133,6 +136,23 @@ function resolveVendorUrl(): string { return `http://localhost:${port}`; } +/** + * Base URL for Saleor Dashboard (models, orders). Derived from `{NEXT_PUBLIC_SALEOR_URL origin}/dashboard`. + */ +function resolveSaleorDashboardBaseUrl(): string | null { + const saleorUrl = env.NEXT_PUBLIC_SALEOR_URL; + + if (!saleorUrl) { + return null; + } + + try { + return `${new URL(saleorUrl).origin}/dashboard`; + } catch { + return null; + } +} + export const config = { isDev: env.MARKETPLACE_NODE_ENV === "development", isProd: env.MARKETPLACE_NODE_ENV === "production", @@ -160,6 +180,9 @@ export const config = { }, }, saleor: { + get dashboardBaseUrl(): string | null { + return resolveSaleorDashboardBaseUrl(); + }, url: env.NEXT_PUBLIC_SALEOR_URL, channelSlug: env.NEXT_PUBLIC_SALEOR_MARKETPLACE_CHANNEL_SLUG, graphqlUrl: env.NEXT_PUBLIC_GRAPHQL_URL, @@ -187,4 +210,7 @@ export const config = { webhookSecret: env.MARKETPLACE_STRIPE_CONNECT_WEBHOOK_SECRET, defaultCountry: env.MARKETPLACE_STRIPE_CONNECT_DEFAULT_COUNTRY, }, + ledger: { + databaseUrl: env.DATABASE_URL, + }, } as const; diff --git a/apps/marketplace/src/lib/graphql/server/schema.ts b/apps/marketplace/src/lib/graphql/server/schema.ts index fd8a24ae..a1530576 100644 --- a/apps/marketplace/src/lib/graphql/server/schema.ts +++ b/apps/marketplace/src/lib/graphql/server/schema.ts @@ -294,6 +294,117 @@ export async function getStitchedSchema() { }); }, }, + draftOrderUpdate: { + resolve(_source, args, context: ServerContext, info) { + const vendorId = requireVendorID(context); + const input = args?.input + ? withVendorMetadata(args.input, vendorId) + : args?.input; + + return delegateToSchema({ + schema: saleorSchema, + operation: OperationTypeNode.MUTATION, + fieldName: "draftOrderUpdate", + args: { ...args, input }, + context, + info, + }); + }, + }, + draftOrderComplete: { + resolve: async (_source, args, context: ServerContext, info) => { + const vendorId = requireVendorID(context); + + const result = (await delegateToSchema({ + schema: saleorSchema, + operation: OperationTypeNode.MUTATION, + fieldName: "draftOrderComplete", + args, + context, + info, + })) as { + data?: { + draftOrderComplete?: { + errors?: Array<{ message?: string | null }>; + order?: { id?: string } | null; + }; + }; + errors?: ReadonlyArray<{ message?: string }>; + }; + + if (result.errors?.length) { + return result; + } + + const payload = result.data?.draftOrderComplete; + + if (!payload?.order?.id || (payload.errors?.length ?? 0) > 0) { + return result; + } + + const updateMetaDoc = parse(` + mutation EnsureOrderVendorMetadata($id: ID!, $input: [MetadataInput!]!) { + updateMetadata(id: $id, input: $input) { + errors { + field + message + code + } + } + } + `); + + const metaResult = (await saleorSchema.executor!({ + document: updateMetaDoc, + variables: { + id: payload.order.id, + input: [{ key: METADATA_KEYS.VENDOR_ID, value: vendorId }], + }, + context, + })) as { + data?: { + updateMetadata?: { + errors?: Array<{ message?: string | null }>; + }; + }; + errors?: ReadonlyArray<{ message?: string }>; + }; + + if ( + metaResult.errors?.length || + (metaResult.data?.updateMetadata?.errors?.length ?? 0) > 0 + ) { + console.error( + "[draftOrderComplete] Failed to set vendor.id on order (order-created webhook can still fix)", + { + errors: + metaResult.errors ?? + metaResult.data?.updateMetadata?.errors, + orderId: payload.order.id, + }, + ); + } + + return result; + }, + }, + orderUpdate: { + resolve(_source, args, context: ServerContext, info) { + const vendorId = requireVendorID(context); + const input = args?.input + ? withVendorMetadata(args.input, vendorId) + : args?.input; + + return delegateToSchema({ + schema: saleorSchema, + operation: OperationTypeNode.MUTATION, + fieldName: "orderUpdate", + args: { ...args, input }, + context, + info, + }); + }, + }, productBulkCreate: { resolve(_source, args, context: ServerContext, info) { const vendorId = requireVendorID(context); diff --git a/apps/marketplace/src/lib/ledger/close-payout-batch.ts b/apps/marketplace/src/lib/ledger/close-payout-batch.ts new file mode 100644 index 00000000..bb6b93d9 --- /dev/null +++ b/apps/marketplace/src/lib/ledger/close-payout-batch.ts @@ -0,0 +1,48 @@ +import { getLedgerPool } from "@/lib/ledger/pool"; +import { closePayoutBatchAndCreateItems } from "@/lib/ledger/repository"; + +export type ClosePayoutBatchInput = { + createdBy: string; + currency: string; + periodEnd: string; + periodStart: string; +}; + +export type ClosePayoutBatchResult = + | { + batchId: string; + itemCount: number; + ok: true; + } + | { + ok: false; + reason: "no_database" | "no_eligible_lines"; + }; + +/** + * Lock period and insert payout_batch_items from eligible ledger order_gross rows. + */ +export async function closePayoutBatchForPeriod( + input: ClosePayoutBatchInput, +): Promise { + const pool = getLedgerPool(); + + if (!pool) { + return { ok: false, reason: "no_database" }; + } + + const cutoff = new Date(); + const created = await closePayoutBatchAndCreateItems(pool, { + createdBy: input.createdBy, + currency: input.currency, + cutoff, + periodEnd: input.periodEnd, + periodStart: input.periodStart, + }); + + if (created === null) { + return { ok: false, reason: "no_eligible_lines" }; + } + + return { batchId: created.batchId, itemCount: created.itemCount, ok: true }; +} diff --git a/apps/marketplace/src/lib/ledger/execute-payout-batch.ts b/apps/marketplace/src/lib/ledger/execute-payout-batch.ts new file mode 100644 index 00000000..a9f94de2 --- /dev/null +++ b/apps/marketplace/src/lib/ledger/execute-payout-batch.ts @@ -0,0 +1,131 @@ +import { getLedgerPool } from "@/lib/ledger/pool"; +import { + countPayoutBatchItemsByStatus, + insertStripeTransferRecord, + listPayoutBatchItemsForExecution, + setPayoutBatchStatus, + updatePayoutBatchItemStatus, +} from "@/lib/ledger/repository"; +import { createStripeConnectTransfer } from "@/lib/stripe/payout-api"; + +export type ExecutePayoutBatchResult = { + batchStatus: string; + errors: Array<{ itemId: string; message: string }>; + processed: number; +}; + +/** + * Create Stripe transfers for each batch item in `ready` status with net_minor > 0. + */ +export async function executePayoutBatchTransfers( + batchId: string, +): Promise { + const pool = getLedgerPool(); + + if (!pool) { + throw new Error("Ledger database not configured"); + } + + const batchRow = await pool.query<{ status: string }>( + `select status::text as status from payout_batches where id = $1::uuid`, + [batchId], + ); + + const batchStatus = batchRow.rows[0]?.status; + + if ( + batchStatus !== "locked" && + batchStatus !== "partially_paid" && + batchStatus !== "executing" + ) { + throw new Error( + `Batch must be locked or partially_paid before execution (got ${batchStatus ?? "unknown"})`, + ); + } + + await setPayoutBatchStatus(pool, { batchId, status: "executing" }); + + const items = await listPayoutBatchItemsForExecution(pool, batchId); + + if (items.length === 0) { + await setPayoutBatchStatus(pool, { + batchId, + executedAt: new Date(), + status: "paid", + }); + + return { batchStatus: "paid", errors: [], processed: 0 }; + } + + const errors: Array<{ itemId: string; message: string }> = []; + let processed = 0; + + for (const item of items) { + const netMinor = BigInt(item.net_minor); + + if (netMinor <= 0n) { + continue; + } + + const idempotencyKey = `payout:${batchId}:item:${item.id}`; + + try { + const transfer = await createStripeConnectTransfer({ + amountMinor: Number(netMinor), + currency: item.currency, + destinationAccountId: item.destination_account, + idempotencyKey, + metadata: { + batch_id: batchId, + vendor_id: item.vendor_id, + }, + transferGroup: `payout:${batchId}`, + }); + + await insertStripeTransferRecord(pool, { + amountMinor: netMinor, + currency: item.currency, + destinationAccount: item.destination_account, + idempotencyKey, + payoutBatchItemId: item.id, + stripeTransferId: transfer.id, + transferGroup: `payout:${batchId}`, + }); + await updatePayoutBatchItemStatus(pool, { + itemId: item.id, + status: "paid", + }); + processed += 1; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + + errors.push({ itemId: item.id, message }); + await updatePayoutBatchItemStatus(pool, { + failureReason: message, + itemId: item.id, + status: "failed", + }); + } + } + + const counts = await countPayoutBatchItemsByStatus(pool, batchId); + let finalBatchStatus = "paid"; + + if (counts.failed > 0) { + finalBatchStatus = counts.paid > 0 ? "partially_paid" : "failed"; + } else if (counts.ready > 0) { + finalBatchStatus = "executing"; + } + + await setPayoutBatchStatus(pool, { + batchId, + executedAt: new Date(), + status: finalBatchStatus, + }); + + return { + batchStatus: finalBatchStatus, + errors, + processed, + }; +} diff --git a/apps/marketplace/src/lib/ledger/ingest-order-paid.ts b/apps/marketplace/src/lib/ledger/ingest-order-paid.ts new file mode 100644 index 00000000..c37237b8 --- /dev/null +++ b/apps/marketplace/src/lib/ledger/ingest-order-paid.ts @@ -0,0 +1,47 @@ +import { getLedgerPool } from "@/lib/ledger/pool"; +import { insertOrderGrossLedgerEntry } from "@/lib/ledger/repository"; + +/** + * MVP: assumes 2-decimal fiat; extend for zero-decimal currencies (e.g. JPY) when needed. + */ +export function saleorGrossToMinorUnits(amount: string | number): bigint { + const n = typeof amount === "number" ? amount : Number.parseFloat(amount); + + if (!Number.isFinite(n)) { + throw new Error("Invalid order gross amount"); + } + + return BigInt(Math.round(n * 100)); +} + +export type IngestOrderPaidInput = { + currency: string; + grossAmount: string | number; + occurredAt: Date; + orderId: string; + stripeChargeId: string | null; + vendorId: string; +}; + +export async function ingestOrderPaidToLedger( + input: IngestOrderPaidInput, +): Promise<{ reason?: string; status: "recorded" | "skipped" }> { + const pool = getLedgerPool(); + + if (!pool) { + return { reason: "DATABASE_URL not configured", status: "skipped" }; + } + + const amountMinor = saleorGrossToMinorUnits(input.grossAmount); + + await insertOrderGrossLedgerEntry(pool, { + amountMinor, + currency: input.currency, + occurredAt: input.occurredAt, + orderId: input.orderId, + stripeChargeId: input.stripeChargeId, + vendorId: input.vendorId, + }); + + return { status: "recorded" }; +} diff --git a/apps/marketplace/src/lib/ledger/link-orders-stripe-charge.ts b/apps/marketplace/src/lib/ledger/link-orders-stripe-charge.ts new file mode 100644 index 00000000..53f16c13 --- /dev/null +++ b/apps/marketplace/src/lib/ledger/link-orders-stripe-charge.ts @@ -0,0 +1,85 @@ +import { MetadataUpdateDocument } from "@/graphql/generated/client"; +import { executeGraphQL } from "@/lib/graphql/execute"; +import { getLedgerPool } from "@/lib/ledger/pool"; +import { updateLedgerStripeChargeForOrders } from "@/lib/ledger/repository"; +import { METADATA_KEYS } from "@/lib/saleor/consts"; +import { getStripeClient } from "@/lib/stripe/client"; +import { marketplaceLogger } from "@/services/logging"; + +/** Resolve Stripe Charge id (ch_…) for a PaymentIntent (pi_…). */ +export async function getChargeIdFromPaymentIntentId( + paymentIntentId: string, +): Promise { + const stripe = getStripeClient(); + const pi = await stripe.paymentIntents.retrieve(paymentIntentId, { + expand: ["latest_charge"], + }); + const lc = pi.latest_charge; + + if (typeof lc === "string") { + return lc; + } + + if (lc && typeof lc === "object" && "id" in lc) { + return (lc as { id: string }).id; + } + + marketplaceLogger.warning( + "[stripe] PaymentIntent has no latest_charge; ledger may miss charge.succeeded linkage", + { paymentIntentId }, + ); + + return null; +} + +/** + * After payment_intent.succeeded: resolve Charge id (ch_…) and attach to Saleor orders + ledger. + * Connect webhook charge.succeeded updates settlement by stripe_charge_id on ledger_entries. + */ +export async function linkOrdersToStripeChargeFromPaymentIntent(input: { + authToken: string; + orderIds: string[]; + paymentIntentId: string; +}): Promise<{ chargeId: string } | null> { + const uniqueOrderIds = [...new Set(input.orderIds.filter(Boolean))]; + + if (uniqueOrderIds.length === 0) { + return null; + } + + const chargeId = await getChargeIdFromPaymentIntentId(input.paymentIntentId); + + if (!chargeId) { + return null; + } + + const pool = getLedgerPool(); + + if (pool) { + await updateLedgerStripeChargeForOrders(pool, { + chargeId, + orderIds: uniqueOrderIds, + }); + } + + for (const orderId of uniqueOrderIds) { + const metaResult = await executeGraphQL( + MetadataUpdateDocument, + "MetadataUpdateMutation", + { + id: orderId, + input: [{ key: METADATA_KEYS.STRIPE_CHARGE_ID, value: chargeId }], + }, + input.authToken, + ); + + if (!metaResult.ok) { + marketplaceLogger.warning( + "[stripe] updateMetadata stripe_charge_id failed for order", + { errors: metaResult.errors, orderId }, + ); + } + } + + return { chargeId }; +} diff --git a/apps/marketplace/src/lib/ledger/pool.ts b/apps/marketplace/src/lib/ledger/pool.ts new file mode 100644 index 00000000..d99a9dde --- /dev/null +++ b/apps/marketplace/src/lib/ledger/pool.ts @@ -0,0 +1,19 @@ +import { Pool } from "pg"; + +import { config } from "@/lib/config"; + +let pool: Pool | null = null; + +export function getLedgerPool(): Pool | null { + const url = config.ledger.databaseUrl; + + if (!url) { + return null; + } + + if (!pool) { + pool = new Pool({ connectionString: url, max: 8 }); + } + + return pool; +} diff --git a/apps/marketplace/src/lib/ledger/repository.ts b/apps/marketplace/src/lib/ledger/repository.ts new file mode 100644 index 00000000..909c6def --- /dev/null +++ b/apps/marketplace/src/lib/ledger/repository.ts @@ -0,0 +1,843 @@ +import crypto from "node:crypto"; + +import type { Pool } from "pg"; + +/** Pool or transaction client — both implement `.query`. */ +type LedgerQueryable = Pick; + +export type InsertOrderGrossInput = { + amountMinor: bigint; + currency: string; + /** Prefer Saleor order.created; else webhook processing time. */ + occurredAt: Date; + orderId: string; + stripeChargeId: string | null; + vendorId: string; +}; + +/** + * Insert ORDER_PAID gross line; idempotent on (source_system, source_ref, entry_type). + */ +/** + * Attach Stripe Charge id (ch_…) to ledger rows for these Saleor orders. + * Idempotent: only fills null stripe_charge_id. + */ +export async function updateLedgerStripeChargeForOrders( + pool: Pool, + input: { chargeId: string; orderIds: string[] }, +): Promise { + if (input.orderIds.length === 0) { + return; + } + + await pool.query( + `update ledger_entries + set stripe_charge_id = $2 + where entry_type = 'order_gross' + and order_id = any($1::text[]) + and stripe_charge_id is null`, + [input.orderIds, input.chargeId], + ); +} + +export async function insertOrderGrossLedgerEntry( + pool: Pool, + input: InsertOrderGrossInput, +): Promise { + const sourceRef = `order:${input.orderId}:paid:order_gross`; + + await pool.query( + `insert into ledger_entries ( + vendor_id, order_id, currency, amount_minor, entry_type, source_system, source_ref, + stripe_charge_id, funds_status, occurred_at + ) values ($1, $2, $3, $4, 'order_gross', 'saleor', $5, $6, 'pending_stripe', $7) + on conflict (source_system, source_ref, entry_type) do nothing`, + [ + input.vendorId, + input.orderId, + input.currency.toLowerCase(), + input.amountMinor.toString(), + sourceRef, + input.stripeChargeId, + input.occurredAt, + ], + ); +} + +export async function tryInsertStripeWebhookEvent( + pool: Pool, + input: { + eventType: string; + livemode: boolean; + payloadJson: unknown; + payloadRaw: string; + stripeEventId: string; + }, +): Promise<{ id: string } | null> { + const payloadSha256 = crypto + .createHash("sha256") + .update(input.payloadRaw, "utf8") + .digest("hex"); + + const result = await pool.query<{ id: string }>( + `insert into stripe_webhook_events ( + stripe_event_id, event_type, livemode, payload_json, payload_sha256 + ) values ($1, $2, $3, $4::jsonb, $5) + on conflict (stripe_event_id) do nothing + returning id`, + [ + input.stripeEventId, + input.eventType, + input.livemode, + JSON.stringify(input.payloadJson), + payloadSha256, + ], + ); + + return result.rows[0] ?? null; +} + +export async function markStripeWebhookProcessed( + pool: Pool, + input: { result: string; stripeEventId: string }, +): Promise { + await pool.query( + `update stripe_webhook_events + set processed_at = now(), processing_result = $2 + where stripe_event_id = $1`, + [input.stripeEventId, input.result], + ); +} + +export async function updateLedgerSettlementForCharge( + pool: Pool, + input: { + availableOn: Date | null; + balanceTransactionId: string | null; + stripeChargeId: string; + }, +): Promise { + await pool.query( + `update ledger_entries + set + available_on = coalesce($2::timestamptz, available_on), + stripe_balance_transaction_id = coalesce($3, stripe_balance_transaction_id), + funds_status = case + when $2::timestamptz is not null and $2::timestamptz <= now() then 'available'::ledger_funds_status + when $2::timestamptz is not null then 'pending_stripe'::ledger_funds_status + else funds_status + end + where stripe_charge_id = $1`, + [input.stripeChargeId, input.availableOn, input.balanceTransactionId], + ); +} + +/** Unconsumed order_gross lines still tied to a Stripe charge (for admin sync). */ +export async function listDistinctStripeChargeIdsForSync( + pool: Pool, + input: { limit: number }, +): Promise { + const result = await pool.query<{ stripe_charge_id: string }>( + `select distinct stripe_charge_id + from ledger_entries + where entry_type = 'order_gross' + and stripe_charge_id is not null + and consumed_in_batch_id is null + order by stripe_charge_id + limit $1`, + [input.limit], + ); + + return result.rows.map((r) => r.stripe_charge_id); +} + +/** + * Promote pending lines when available_on is already in the past (no extra Stripe call). + */ +export async function promoteOrderGrossPendingWhenAvailableOnReached( + pool: Pool, +): Promise { + const result = await pool.query( + `update ledger_entries + set funds_status = 'available'::ledger_funds_status + where entry_type = 'order_gross' + and funds_status = 'pending_stripe' + and available_on is not null + and available_on <= now()`, + ); + + return result.rowCount ?? 0; +} + +export async function insertPlatformBalanceSnapshot( + pool: Pool, + input: { + availableMinor: bigint; + currency: string; + pendingMinor: bigint; + sourceType: string; + }, +): Promise { + await pool.query( + `insert into balance_snapshots ( + account_scope, stripe_account_id, currency, available_minor, pending_minor, source_type + ) values ('platform', null, $1, $2, $3, $4)`, + [ + input.currency.toLowerCase(), + input.availableMinor.toString(), + input.pendingMinor.toString(), + input.sourceType, + ], + ); +} + +const stripeToTransferStatus: Record = { + paid: "paid", + pending: "created", + in_transit: "in_transit", + canceled: "canceled", + failed: "failed", + reversed: "reversed", +}; + +export async function updateStripeTransferRowStatus( + pool: Pool, + input: { stripeStatus: string; stripeTransferId: string }, +): Promise { + const mapped = stripeToTransferStatus[input.stripeStatus] ?? "created"; + + await pool.query( + `update stripe_transfers + set status = $1::stripe_transfer_status, updated_at = now() + where stripe_transfer_id = $2`, + [mapped, input.stripeTransferId], + ); +} + +export async function listRecentPayoutBatches( + pool: Pool, + limit: number, +): Promise< + Array<{ + created_at: Date; + currency: string; + executed_at: Date | null; + id: string; + period_end: string; + period_start: string; + status: string; + transfer_initiated_at: Date | null; + }> +> { + const result = await pool.query<{ + created_at: Date; + currency: string; + executed_at: Date | null; + id: string; + period_end: string; + period_start: string; + status: string; + transfer_initiated_at: Date | null; + }>( + `select + pb.id::text as id, + to_char(pb.period_start, 'YYYY-MM-DD') as period_start, + to_char(pb.period_end, 'YYYY-MM-DD') as period_end, + pb.currency, + pb.status::text as status, + pb.created_at, + pb.executed_at, + ti.first_transfer_at as transfer_initiated_at + from payout_batches pb + left join lateral ( + select min(st.created_at) as first_transfer_at + from payout_batch_items pbi + inner join stripe_transfers st on st.payout_batch_item_id = pbi.id + where pbi.batch_id = pb.id + ) ti on true + order by pb.created_at desc + limit $1`, + [limit], + ); + + return result.rows; +} + +/** + * Marks ledger lines as belonging to this batch. Call only from + * {@link populatePayoutBatchItemsFromLedger} when the batch is created — never from GET / overview / + * execute, or new lines can be incorrectly tied to old paid batches. + */ +export async function ensureLedgerEntriesConsumedForBatch( + db: LedgerQueryable, + batchId: string, +): Promise { + const batchResult = await db.query<{ + currency: string; + locked_at: Date | null; + period_end: Date | string; + period_start: Date | string; + status: string; + }>( + `select currency, locked_at, period_start, period_end, status::text as status + from payout_batches where id = $1::uuid`, + [batchId], + ); + + const batch = batchResult.rows[0]; + + if (!batch || batch.status === "canceled") { + return; + } + + await db.query( + `update ledger_entries le + set consumed_in_batch_id = $1::uuid + where le.consumed_in_batch_id is null + and le.entry_type = 'order_gross' + and le.funds_status = 'available' + and lower(le.currency) = lower($2::text) + and le.occurred_at::date >= $3::date + and le.occurred_at::date <= $4::date + and le.vendor_id in ( + select vendor_id from payout_batch_items where batch_id = $1::uuid + )`, + [batchId, batch.currency, batch.period_start, batch.period_end], + ); +} + +export type PopulatePayoutBatchItemsInput = { + batchId: string; + currency: string; + cutoff: Date; + periodEnd: string; + periodStart: string; +}; + +/** + * Insert payout_batch_items from eligible ledger rows and mark them consumed. + * Eligibility: funds_status = 'available' (authoritative), period + currency, not yet consumed. + */ +export async function populatePayoutBatchItemsFromLedger( + db: LedgerQueryable, + input: PopulatePayoutBatchItemsInput, +): Promise { + const insertItems = await db.query( + `insert into payout_batch_items ( + batch_id, vendor_id, stripe_account_id, currency, + gross_minor, fees_minor, net_minor, ledger_cutoff_ts, status + ) + select $1::uuid, agg.vendor_id, + coalesce(v.stripe_account_id, '__unconnected__'), + lower($2::text), + agg.gross_minor, 0::bigint, agg.gross_minor, $3::timestamptz, + case + when v.vendor_id is null then 'skipped'::payout_item_status + when v.payouts_enabled then 'ready'::payout_item_status + else 'skipped'::payout_item_status + end + from ( + select vendor_id, sum(amount_minor)::bigint as gross_minor + from ledger_entries + where entry_type = 'order_gross' + and funds_status = 'available' + and consumed_in_batch_id is null + and lower(currency) = lower($2::text) + and occurred_at::date >= $4::date + and occurred_at::date <= $5::date + group by vendor_id + having sum(amount_minor) > 0 + ) agg + left join vendor_stripe_accounts v on v.vendor_id = agg.vendor_id`, + [ + input.batchId, + input.currency, + input.cutoff, + input.periodStart, + input.periodEnd, + ], + ); + + await ensureLedgerEntriesConsumedForBatch(db, input.batchId); + + return insertItems.rowCount ?? 0; +} + +export async function getPayoutBatchWithItems( + pool: Pool, + batchId: string, +): Promise<{ + batch: { + created_at: Date; + currency: string; + executed_at: Date | null; + id: string; + period_end: string; + period_start: string; + status: string; + } | null; + items: Array<{ + fees_minor: string; + gross_minor: string; + id: string; + net_minor: string; + status: string; + stripe_transfer_created_at: Date | null; + vendor_id: string; + }>; +}> { + const batchResult = await pool.query<{ + created_at: Date; + currency: string; + executed_at: Date | null; + id: string; + period_end: string; + period_start: string; + status: string; + }>( + `select id::text, + to_char(period_start, 'YYYY-MM-DD') as period_start, + to_char(period_end, 'YYYY-MM-DD') as period_end, + currency, + status::text as status, + created_at, + executed_at + from payout_batches where id = $1::uuid`, + [batchId], + ); + + const batch = batchResult.rows[0] ?? null; + + if (!batch) { + return { batch: null, items: [] }; + } + + const itemsResult = await pool.query<{ + fees_minor: string; + gross_minor: string; + id: string; + net_minor: string; + status: string; + stripe_transfer_created_at: Date | null; + vendor_id: string; + }>( + `select pbi.id::text as id, + pbi.vendor_id, + pbi.gross_minor::text, + pbi.fees_minor::text, + pbi.net_minor::text, + pbi.status::text as status, + min(st.created_at) as stripe_transfer_created_at + from payout_batch_items pbi + left join stripe_transfers st on st.payout_batch_item_id = pbi.id + where pbi.batch_id = $1::uuid + group by pbi.id, pbi.vendor_id, pbi.gross_minor, pbi.fees_minor, pbi.net_minor, pbi.status + order by pbi.vendor_id`, + [batchId], + ); + + return { batch, items: itemsResult.rows }; +} + +export async function getVendorLedgerSummary( + pool: Pool, + vendorId: string, +): Promise<{ + available_minor: bigint; + currency: string | null; + pending_minor: bigint; +}> { + const result = await pool.query<{ + available_minor: string | null; + currency: string | null; + pending_minor: string | null; + }>( + `select + coalesce(sum(case when funds_status = 'pending_stripe' then amount_minor else 0 end), 0)::text as pending_minor, + coalesce(sum(case + when funds_status = 'available' and consumed_in_batch_id is null then amount_minor + else 0 + end), 0)::text as available_minor, + max(currency) as currency + from ledger_entries + where vendor_id = $1 and entry_type = 'order_gross'`, + [vendorId], + ); + + const row = result.rows[0]; + + return { + available_minor: BigInt(String(row?.available_minor ?? "0")), + currency: row?.currency ?? null, + pending_minor: BigInt(String(row?.pending_minor ?? "0")), + }; +} + +export type VendorLedgerLineRow = { + amount_minor: string; + available_on: Date | null; + consumed_in_batch_id: string | null; + currency: string; + entry_type: string; + funds_status: string; + id: string; + occurred_at: Date; + order_id: string | null; + stripe_charge_id: string | null; + vendor_id: string; +}; + +const payoutQueueFundsStatuses = `le.funds_status::text in ('pending_stripe', 'available', 'held')`; + +/** Open batch or batch not yet paid out to vendors (hide rows tied only to a paid batch). */ +const payoutQueueConsumptionClause = `( + le.consumed_in_batch_id is null + or not exists ( + select 1 + from payout_batches pb + where pb.id = le.consumed_in_batch_id + and pb.status::text = 'paid' + ) + )`; + +/** + * Order gross lines still owed to vendors (pending Stripe, available, held). + * Excludes refunded/reversed. Includes rows linked to a non-paid batch so + * pending_stripe lines are not hidden when consumed_in_batch_id was set (e.g. backfill). + */ +export async function listVendorLedgerLines( + pool: Pool, + vendorId: string, + input: { limit: number }, +): Promise { + const result = await pool.query<{ + amount_minor: string; + available_on: Date | null; + consumed_in_batch_id: string | null; + currency: string; + entry_type: string; + funds_status: string; + id: string; + occurred_at: Date; + order_id: string | null; + stripe_charge_id: string | null; + vendor_id: string; + }>( + `select le.id::text, + le.vendor_id, + le.order_id, + le.entry_type::text as entry_type, + le.amount_minor::text, + le.currency, + le.funds_status::text as funds_status, + le.available_on, + le.stripe_charge_id, + le.occurred_at, + le.consumed_in_batch_id::text as consumed_in_batch_id + from ledger_entries le + where le.vendor_id = $1 + and le.entry_type = 'order_gross' + and ${payoutQueueFundsStatuses} + and ${payoutQueueConsumptionClause} + order by + case le.funds_status::text + when 'available' then 0 + when 'pending_stripe' then 1 + when 'held' then 2 + else 3 + end, + le.occurred_at desc + limit $2`, + [vendorId, input.limit], + ); + + return result.rows; +} + +/** Operator / dashboard: same queue as {@link listVendorLedgerLines} for all vendors. */ +export async function listAllLedgerLines( + pool: Pool, + input: { limit: number }, +): Promise { + const result = await pool.query<{ + amount_minor: string; + available_on: Date | null; + consumed_in_batch_id: string | null; + currency: string; + entry_type: string; + funds_status: string; + id: string; + occurred_at: Date; + order_id: string | null; + stripe_charge_id: string | null; + vendor_id: string; + }>( + `select le.id::text, + le.vendor_id, + le.order_id, + le.entry_type::text as entry_type, + le.amount_minor::text, + le.currency, + le.funds_status::text as funds_status, + le.available_on, + le.stripe_charge_id, + le.occurred_at, + le.consumed_in_batch_id::text as consumed_in_batch_id + from ledger_entries le + where le.entry_type = 'order_gross' + and ${payoutQueueFundsStatuses} + and ${payoutQueueConsumptionClause} + order by + case le.funds_status::text + when 'available' then 0 + when 'pending_stripe' then 1 + when 'held' then 2 + else 3 + end, + le.occurred_at desc + limit $1`, + [input.limit], + ); + + return result.rows; +} + +export async function upsertVendorStripeAccount( + pool: Pool, + input: { + defaultCurrency: string; + onboardingCompleted: boolean; + payoutsEnabled: boolean; + stripeAccountId: string; + vendorId: string; + }, +): Promise { + await pool.query( + `insert into vendor_stripe_accounts ( + vendor_id, stripe_account_id, payouts_enabled, onboarding_completed, default_currency + ) values ($1, $2, $3, $4, lower($5)) + on conflict (vendor_id) do update set + stripe_account_id = excluded.stripe_account_id, + payouts_enabled = excluded.payouts_enabled, + onboarding_completed = excluded.onboarding_completed, + default_currency = excluded.default_currency, + updated_at = now()`, + [ + input.vendorId, + input.stripeAccountId, + input.payoutsEnabled, + input.onboardingCompleted, + input.defaultCurrency, + ], + ); +} + +/** + * Create another payout batch for the same (period, currency) when new eligible lines exist. + * Only creates a batch when at least one eligible `available` order_gross line exists for the window. + * Items + consumed_in_batch_id come from the same eligibility rules as {@link populatePayoutBatchItemsFromLedger}. + */ +export async function closePayoutBatchAndCreateItems( + pool: Pool, + input: { + createdBy: string; + currency: string; + cutoff: Date; + periodEnd: string; + periodStart: string; + }, +): Promise<{ batchId: string; itemCount: number } | null> { + const client = await pool.connect(); + + try { + await client.query("begin"); + + const eligible = await client.query<{ ok: boolean }>( + `select exists ( + select 1 + from ledger_entries + where entry_type = 'order_gross' + and funds_status = 'available' + and consumed_in_batch_id is null + and lower(currency) = lower($1::text) + and occurred_at::date >= $2::date + and occurred_at::date <= $3::date + ) as ok`, + [input.currency, input.periodStart, input.periodEnd], + ); + + if (!eligible.rows[0]?.ok) { + await client.query("rollback"); + + return null; + } + + const batchResult = await client.query<{ id: string }>( + `insert into payout_batches ( + period_start, period_end, currency, status, locked_at, created_by + ) values ($1::date, $2::date, lower($3), 'locked', $4::timestamptz, $5) + returning id`, + [ + input.periodStart, + input.periodEnd, + input.currency, + input.cutoff, + input.createdBy, + ], + ); + + const batchId = batchResult.rows[0]?.id; + + if (!batchId) { + throw new Error("Failed to create payout batch"); + } + + const itemCount = await populatePayoutBatchItemsFromLedger(client, { + batchId, + currency: input.currency, + cutoff: input.cutoff, + periodEnd: input.periodEnd, + periodStart: input.periodStart, + }); + + if (itemCount === 0) { + await client.query("rollback"); + + return null; + } + + await client.query("commit"); + + return { batchId, itemCount }; + } catch (error) { + await client.query("rollback"); + throw error; + } finally { + client.release(); + } +} + +export type PayoutBatchItemRow = { + currency: string; + destination_account: string; + id: string; + net_minor: string; + status: string; + vendor_id: string; +}; + +export async function listPayoutBatchItemsForExecution( + pool: Pool, + batchId: string, +): Promise { + const result = await pool.query( + `select + pbi.id, + pbi.vendor_id, + pbi.stripe_account_id as destination_account, + lower(pbi.currency) as currency, + pbi.net_minor::text as net_minor, + pbi.status::text as status + from payout_batch_items pbi + where pbi.batch_id = $1::uuid + and pbi.status = 'ready' + and pbi.net_minor > 0 + order by pbi.vendor_id`, + [batchId], + ); + + return result.rows; +} + +export async function insertStripeTransferRecord( + pool: Pool, + input: { + amountMinor: bigint; + currency: string; + destinationAccount: string; + idempotencyKey: string; + payoutBatchItemId: string; + stripeTransferId: string; + transferGroup: string; + }, +): Promise { + await pool.query( + `insert into stripe_transfers ( + payout_batch_item_id, stripe_transfer_id, destination_account, + transfer_group, amount_minor, currency, idempotency_key, status + ) values ($1::uuid, $2, $3, $4, $5, lower($6), $7, 'created')`, + [ + input.payoutBatchItemId, + input.stripeTransferId, + input.destinationAccount, + input.transferGroup, + input.amountMinor.toString(), + input.currency, + input.idempotencyKey, + ], + ); +} + +export async function updatePayoutBatchItemStatus( + pool: Pool, + input: { + failureReason?: string | null; + itemId: string; + status: string; + }, +): Promise { + await pool.query( + `update payout_batch_items + set status = $2::payout_item_status, + failure_reason = $3 + where id = $1::uuid`, + [input.itemId, input.status, input.failureReason ?? null], + ); +} + +export async function setPayoutBatchStatus( + pool: Pool, + input: { + batchId: string; + executedAt?: Date | null; + status: string; + }, +): Promise { + await pool.query( + `update payout_batches + set status = $2::payout_batch_status, + executed_at = coalesce($3::timestamptz, executed_at) + where id = $1::uuid`, + [input.batchId, input.status, input.executedAt ?? null], + ); +} + +export async function countPayoutBatchItemsByStatus( + pool: Pool, + batchId: string, +): Promise<{ failed: number; paid: number; ready: number; skipped: number }> { + const result = await pool.query<{ + failed: string; + paid: string; + ready: string; + skipped: string; + }>( + `select + coalesce(sum(case when status = 'failed' then 1 else 0 end), 0)::text as failed, + coalesce(sum(case when status = 'paid' then 1 else 0 end), 0)::text as paid, + coalesce(sum(case when status = 'ready' then 1 else 0 end), 0)::text as ready, + coalesce(sum(case when status = 'skipped' then 1 else 0 end), 0)::text as skipped + from payout_batch_items + where batch_id = $1::uuid`, + [batchId], + ); + + const row = result.rows[0]; + + return { + failed: Number(row?.failed ?? 0), + paid: Number(row?.paid ?? 0), + ready: Number(row?.ready ?? 0), + skipped: Number(row?.skipped ?? 0), + }; +} diff --git a/apps/marketplace/src/lib/ledger/sync-ledger-settlement-from-stripe.ts b/apps/marketplace/src/lib/ledger/sync-ledger-settlement-from-stripe.ts new file mode 100644 index 00000000..39ccdb00 --- /dev/null +++ b/apps/marketplace/src/lib/ledger/sync-ledger-settlement-from-stripe.ts @@ -0,0 +1,129 @@ +import { getLedgerPool } from "@/lib/ledger/pool"; +import { + listDistinctStripeChargeIdsForSync, + promoteOrderGrossPendingWhenAvailableOnReached, + updateLedgerSettlementForCharge, +} from "@/lib/ledger/repository"; +import { + retrieveStripeBalanceTransaction, + retrieveStripeCharge, +} from "@/lib/stripe/payout-api"; + +export type SyncLedgerStripeSettlementResult = + | { + chargeErrors: Array<{ chargeId: string; message: string }>; + chargeIdsAttempted: number; + chargesSynced: number; + ok: true; + promotedByDateCount: number; + } + | { ok: false; reason: "no_database" }; + +const DEFAULT_CHARGE_LIMIT = 500; + +async function settlementFromCharge(chargeId: string): Promise<{ + availableOn: Date | null; + balanceTransactionId: string | null; +}> { + const charge = await retrieveStripeCharge(chargeId); + const bt = charge.balance_transaction; + let balanceTransactionId: string | null = null; + let availableOn: Date | null = null; + + if (typeof bt === "string") { + balanceTransactionId = bt; + + const full = await retrieveStripeBalanceTransaction(bt); + + availableOn = new Date(full.available_on * 1000); + } else if (bt && typeof bt === "object") { + const obj = bt as { available_on?: number; id?: string }; + + balanceTransactionId = + typeof obj.id === "string" ? obj.id : balanceTransactionId; + + if (typeof obj.available_on === "number") { + availableOn = new Date(obj.available_on * 1000); + } + } + + return { availableOn, balanceTransactionId }; +} + +async function updateLedgerSettlementFromStripeCharge( + chargeId: string, +): Promise { + const pool = getLedgerPool(); + + if (!pool) { + return; + } + + const { availableOn, balanceTransactionId } = + await settlementFromCharge(chargeId); + + await updateLedgerSettlementForCharge(pool, { + availableOn, + balanceTransactionId, + stripeChargeId: chargeId, + }); +} + +/** + * Pull balance transaction for one charge, update ledger, then promote rows past available_on. + * Call after ORDER_PAID when Stripe charge id is known. + */ +export async function applySettlementForCharge( + chargeId: string, +): Promise { + await updateLedgerSettlementFromStripeCharge(chargeId); + const pool = getLedgerPool(); + + if (!pool) { + return; + } + + await promoteOrderGrossPendingWhenAvailableOnReached(pool); +} + +/** + * Admin / ops: re-pull Balance Transaction data from Stripe for open ledger lines, + * then promote any rows whose available_on is already past. + */ +export async function syncLedgerSettlementFromStripe(input?: { + chargeLimit?: number; +}): Promise { + const pool = getLedgerPool(); + + if (!pool) { + return { ok: false, reason: "no_database" }; + } + + const limit = input?.chargeLimit ?? DEFAULT_CHARGE_LIMIT; + const chargeIds = await listDistinctStripeChargeIdsForSync(pool, { limit }); + const chargeErrors: Array<{ chargeId: string; message: string }> = []; + let chargesSynced = 0; + + for (const chargeId of chargeIds) { + try { + await updateLedgerSettlementFromStripeCharge(chargeId); + chargesSynced += 1; + } catch (error) { + chargeErrors.push({ + chargeId, + message: error instanceof Error ? error.message : String(error), + }); + } + } + + const promotedByDateCount = + await promoteOrderGrossPendingWhenAvailableOnReached(pool); + + return { + chargeErrors, + chargesSynced, + chargeIdsAttempted: chargeIds.length, + ok: true, + promotedByDateCount, + }; +} diff --git a/apps/marketplace/src/lib/saleor/consts.ts b/apps/marketplace/src/lib/saleor/consts.ts index e80354a1..40622ef5 100644 --- a/apps/marketplace/src/lib/saleor/consts.ts +++ b/apps/marketplace/src/lib/saleor/consts.ts @@ -13,6 +13,8 @@ export const METADATA_KEYS = { PAYMENT_ACCOUNT_ID: "payment_account_id", /** Stripe Connect onboarding completion flag for vendor profile */ PAYMENT_ACCOUNT_CONNECTED: "payment_account_connected", + /** Stripe Charge id on the order (settlement / ledger linkage) */ + STRIPE_CHARGE_ID: "stripe_charge_id", /** Vendor-owned customer ids (stored in vendor page metadata as JSON array) */ VENDOR_CUSTOMERS: "meta.customers", /** Default collection flag (stored in collection metadata) */ diff --git a/apps/marketplace/src/lib/saleor/fetch-order-for-ledger.ts b/apps/marketplace/src/lib/saleor/fetch-order-for-ledger.ts new file mode 100644 index 00000000..26e23516 --- /dev/null +++ b/apps/marketplace/src/lib/saleor/fetch-order-for-ledger.ts @@ -0,0 +1,206 @@ +import { METADATA_KEYS } from "@/lib/saleor/consts"; +import { marketplaceLogger } from "@/services/logging"; + +export type OrderLedgerSnapshot = { + /** ISO 8601 from Saleor order.created */ + created?: string | null; + lines: Array<{ + variant?: { + product?: { metadata?: Array<{ key: string; value: string }> } | null; + } | null; + }>; + metadata: Array<{ key: string; value: string }>; + total: { + gross: { amount: number; currency: string }; + } | null; + transactions?: Array<{ + chargedAmount: { amount: number } | null; + pspReference: string | null; + }>; +}; + +function findMeta( + metadata: Array<{ key: string; value: string }> | undefined, + key: string, +): string | null { + const item = metadata?.find((m) => m.key === key); + + return item?.value?.trim() || null; +} + +/** + * Same vendor resolution as order-created: order metadata, then first line product metadata. + */ +export function resolveVendorIdFromOrderSnapshot( + order: OrderLedgerSnapshot, +): string | null { + const fromOrder = + findMeta(order.metadata, METADATA_KEYS.VENDOR_ID) ?? + findMeta(order.metadata, "vendor_id"); + + if (fromOrder) { + return fromOrder; + } + + for (const line of order.lines) { + const productMeta = line.variant?.product?.metadata; + + if (!productMeta?.length) { + continue; + } + + const vid = + findMeta(productMeta, METADATA_KEYS.VENDOR_ID) ?? + findMeta(productMeta, "vendor_id"); + + if (vid) { + return vid; + } + } + + return null; +} + +/** First Saleor transaction whose PSP reference is a Stripe PaymentIntent id. */ +export function pickPaymentIntentIdFromOrderSnapshot( + snapshot: OrderLedgerSnapshot, +): string | null { + const txs = snapshot.transactions; + + if (!txs?.length) { + return null; + } + + let fallbackPi: string | null = null; + + for (const t of txs) { + const ref = t.pspReference?.trim(); + + if (!ref?.startsWith("pi_")) { + continue; + } + + if ((t.chargedAmount?.amount ?? 0) > 0) { + return ref; + } + + if (!fallbackPi) { + fallbackPi = ref; + } + } + + return fallbackPi; +} + +type GraphQLResponse = { + data?: T; + errors?: Array<{ message?: string }>; +}; + +/** + * Load current order from Saleor (avoids race: ORDER_PAID payload before ORDER_CREATED metadata update). + */ +export async function fetchOrderSnapshotForLedger(input: { + authToken: string; + orderId: string; + saleorDomain: string; +}): Promise { + const query = ` + query OrderLedgerSnapshot($id: ID!) { + order(id: $id) { + created + metadata { + key + value + } + total { + gross { + amount + currency + } + } + lines { + variant { + product { + metadata { + key + value + } + } + } + } + transactions { + pspReference + chargedAmount { + amount + } + } + } + } + `; + + const response = await fetch(`https://${input.saleorDomain}/graphql/`, { + body: JSON.stringify({ query, variables: { id: input.orderId } }), + headers: { + Authorization: `Bearer ${input.authToken}`, + "Content-Type": "application/json", + }, + method: "POST", + }); + + const json = (await response.json()) as GraphQLResponse<{ + order?: OrderLedgerSnapshot | null; + }>; + + if (!response.ok || json.errors?.length) { + marketplaceLogger.warning( + "[order-paid] Saleor order snapshot fetch failed", + { + errors: json.errors?.map((e) => e.message), + httpStatus: response.status, + orderId: input.orderId, + }, + ); + + return null; + } + + const order = json.data?.order; + + if (!order) { + marketplaceLogger.warning( + "[order-paid] Saleor order snapshot missing order node", + { + orderId: input.orderId, + }, + ); + + return null; + } + + return { + created: order.created != null ? String(order.created) : null, + lines: order.lines ?? [], + metadata: order.metadata ?? [], + total: order.total?.gross + ? { + gross: { + amount: Number(order.total.gross.amount), + currency: String(order.total.gross.currency), + }, + } + : null, + transactions: (order.transactions ?? []).map( + (t: { + chargedAmount?: { amount?: unknown } | null; + pspReference?: string | null; + }) => ({ + pspReference: t.pspReference ?? null, + chargedAmount: + t.chargedAmount?.amount != null + ? { amount: Number(t.chargedAmount.amount) } + : null, + }), + ), + }; +} diff --git a/apps/marketplace/src/lib/saleor/fetch-vendor-page-titles.ts b/apps/marketplace/src/lib/saleor/fetch-vendor-page-titles.ts new file mode 100644 index 00000000..716c9faf --- /dev/null +++ b/apps/marketplace/src/lib/saleor/fetch-vendor-page-titles.ts @@ -0,0 +1,34 @@ +import { VendorPageStatusDocument } from "@/graphql/generated/client"; +import { executeGraphQL } from "@/lib/graphql/execute"; + +/** + * Resolve vendor profile page titles for dashboard payout UI (best-effort). + */ +export async function fetchVendorTitlesByIds( + ids: string[], + token: string | null, +): Promise> { + if (!token || ids.length === 0) { + return {}; + } + + const unique = [...new Set(ids)]; + const out: Record = {}; + + await Promise.all( + unique.map(async (id) => { + const result = await executeGraphQL( + VendorPageStatusDocument, + "VendorPageStatusQuery", + { id }, + token, + ); + + if (result.ok && result.data.page?.title) { + out[id] = result.data.page.title; + } + }), + ); + + return out; +} diff --git a/apps/marketplace/src/lib/stripe/account-updated-webhook.ts b/apps/marketplace/src/lib/stripe/account-updated-webhook.ts new file mode 100644 index 00000000..a2ff9a81 --- /dev/null +++ b/apps/marketplace/src/lib/stripe/account-updated-webhook.ts @@ -0,0 +1,99 @@ +import { NextResponse } from "next/server"; + +import { METADATA_KEYS } from "@/lib/saleor/consts"; +import { + getVendorPageMetadata, + updateVendorPageMetadata, +} from "@/lib/saleor/vendor-page-metadata"; +import { mergeMetadata } from "@/lib/saleor/vendor-payment-metadata"; +import { isStripeConnectOnboardingCompleted } from "@/lib/stripe/connect"; + +export type StripeAccountUpdatedPayload = { + data?: { + object?: { + details_submitted?: boolean; + id?: string; + metadata?: Record; + requirements?: { + currently_due?: string[]; + }; + }; + }; +}; + +function resolveDefaultSaleorDomain(): string | null { + const raw = process.env.NEXT_PUBLIC_SALEOR_URL; + + if (!raw) { + return null; + } + + try { + return new URL(raw).hostname; + } catch { + return null; + } +} + +export async function handleStripeConnectAccountUpdated( + event: StripeAccountUpdatedPayload, +): Promise { + const account = event.data?.object; + + if (!account?.id) { + return NextResponse.json( + { error: "Missing account id in Stripe event" }, + { status: 400 }, + ); + } + + const vendorPageId = account.metadata?.vendor_id?.trim(); + + if (!vendorPageId) { + return NextResponse.json({ + status: "skipped", + reason: "missing_vendor_id", + }); + } + + const saleorDomainFromMetadata = account.metadata?.saleor_domain?.trim(); + const saleorDomain = saleorDomainFromMetadata || resolveDefaultSaleorDomain(); + + if (!saleorDomain) { + return NextResponse.json( + { error: "Cannot resolve Saleor domain for webhook update" }, + { status: 500 }, + ); + } + + const connected = isStripeConnectOnboardingCompleted({ + details_submitted: account.details_submitted, + requirements: account.requirements, + }); + const currentMetadata = await getVendorPageMetadata({ + saleorDomain, + vendorPageId, + }); + + await updateVendorPageMetadata({ + saleorDomain, + vendorPageId, + metadata: mergeMetadata(currentMetadata, [ + { + key: METADATA_KEYS.PAYMENT_ACCOUNT_ID, + value: account.id, + }, + { + key: METADATA_KEYS.PAYMENT_ACCOUNT_CONNECTED, + value: connected ? "true" : "false", + }, + ]), + }); + + return NextResponse.json({ + status: "processed", + connected, + vendorPageId, + stripeAccountId: account.id, + }); +} diff --git a/apps/marketplace/src/lib/stripe/client.ts b/apps/marketplace/src/lib/stripe/client.ts index bd3ae3f8..84c41c77 100644 --- a/apps/marketplace/src/lib/stripe/client.ts +++ b/apps/marketplace/src/lib/stripe/client.ts @@ -91,7 +91,49 @@ const parseStripeError = (data: unknown, fallbackMessage: string) => { return fallbackMessage; }; +type PaymentIntentRetrieveOutput = { + id: string; + latest_charge?: null | string | { id: string }; +}; + const paymentIntents = { + /** + * GET PaymentIntent with optional expand (e.g. latest_charge for ch_ id). + */ + retrieve: async ( + id: string, + options?: { expand?: string[] }, + ): Promise => { + const expand = options?.expand ?? []; + const qs = + expand.length > 0 + ? `?${expand.map((e) => `expand[]=${encodeURIComponent(e)}`).join("&")}` + : ""; + const response = await fetch( + `https://api.stripe.com/v1/payment_intents/${encodeURIComponent(id)}${qs}`, + { + headers: { + Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`, + }, + method: "GET", + }, + ); + + const data = (await response.json()) as unknown; + + if (!response.ok) { + throw new Error( + parseStripeError(data, "Stripe payment intent retrieve failed."), + ); + } + + if (typeof data !== "object" || data === null || !("id" in data)) { + throw new Error("Invalid Stripe payment intent retrieve response."); + } + + return data as PaymentIntentRetrieveOutput; + }, + create: async ( input: PaymentIntentCreateInput, ): Promise => { diff --git a/apps/marketplace/src/lib/stripe/connect.ts b/apps/marketplace/src/lib/stripe/connect.ts index 96056cd9..5163405d 100644 --- a/apps/marketplace/src/lib/stripe/connect.ts +++ b/apps/marketplace/src/lib/stripe/connect.ts @@ -16,9 +16,11 @@ type StripeRequirements = { }; export type StripeConnectAccount = { + default_currency?: string; details_submitted?: boolean; id: string; metadata?: Record; + payouts_enabled?: boolean; requirements?: StripeRequirements; }; diff --git a/apps/marketplace/src/lib/stripe/payout-api.ts b/apps/marketplace/src/lib/stripe/payout-api.ts new file mode 100644 index 00000000..1280c1a1 --- /dev/null +++ b/apps/marketplace/src/lib/stripe/payout-api.ts @@ -0,0 +1,152 @@ +import { config } from "@/lib/config"; + +type StripeApiError = { + error?: { message?: string }; +}; + +function assertStripeSecretKey(): string { + const secretKey = config.stripeConnect.secretKey; + + if (!secretKey) { + throw new Error("STRIPE_SECRET_KEY is not set"); + } + + return secretKey; +} + +export type StripeBalancePayload = { + available: Array<{ amount: number; currency: string }>; + pending: Array<{ amount: number; currency: string }>; +}; + +export async function getStripePlatformBalance(): Promise { + const secretKey = assertStripeSecretKey(); + const response = await fetch("https://api.stripe.com/v1/balance", { + method: "GET", + headers: { Authorization: `Bearer ${secretKey}` }, + }); + + const body = (await response.json()) as StripeApiError & StripeBalancePayload; + + if (!response.ok) { + throw new Error(body.error?.message ?? "Stripe balance retrieval failed"); + } + + return body; +} + +export type StripeBalanceTransaction = { + available_on: number; + id: string; + status: string; +}; + +export type StripeChargeWithBalanceTransaction = { + balance_transaction?: string | { available_on?: number; id?: string }; + id: string; +}; + +/** + * Platform charge (ch_…) with expanded balance_transaction for available_on. + */ +export async function retrieveStripeCharge( + chargeId: string, +): Promise { + const secretKey = assertStripeSecretKey(); + const response = await fetch( + `https://api.stripe.com/v1/charges/${encodeURIComponent(chargeId)}?expand[]=balance_transaction`, + { + headers: { Authorization: `Bearer ${secretKey}` }, + method: "GET", + }, + ); + + const body = (await response.json()) as StripeApiError & + StripeChargeWithBalanceTransaction; + + if (!response.ok) { + throw new Error(body.error?.message ?? "Stripe charge retrieval failed"); + } + + if (typeof body.id !== "string") { + throw new Error("Invalid Stripe charge response"); + } + + return body; +} + +export async function retrieveStripeBalanceTransaction( + balanceTransactionId: string, +): Promise { + const secretKey = assertStripeSecretKey(); + const response = await fetch( + `https://api.stripe.com/v1/balance_transactions/${balanceTransactionId}`, + { + method: "GET", + headers: { Authorization: `Bearer ${secretKey}` }, + }, + ); + + const body = (await response.json()) as StripeApiError & + StripeBalanceTransaction; + + if (!response.ok) { + throw new Error( + body.error?.message ?? "Stripe balance transaction retrieval failed", + ); + } + + return body; +} + +export type CreateStripeTransferInput = { + amountMinor: number; + currency: string; + destinationAccountId: string; + idempotencyKey: string; + metadata?: Record; + sourceTransaction?: string; + transferGroup: string; +}; + +export async function createStripeConnectTransfer( + input: CreateStripeTransferInput, +): Promise<{ amount: number; currency: string; id: string }> { + const secretKey = assertStripeSecretKey(); + const body = new URLSearchParams(); + + body.append("amount", String(input.amountMinor)); + body.append("currency", input.currency); + body.append("destination", input.destinationAccountId); + body.append("transfer_group", input.transferGroup); + + if (input.sourceTransaction) { + body.append("source_transaction", input.sourceTransaction); + } + + for (const [k, v] of Object.entries(input.metadata ?? {})) { + body.append(`metadata[${k}]`, v); + } + + const response = await fetch("https://api.stripe.com/v1/transfers", { + method: "POST", + headers: { + Authorization: `Bearer ${secretKey}`, + "Content-Type": "application/x-www-form-urlencoded", + "Idempotency-Key": input.idempotencyKey, + }, + body: body.toString(), + }); + + const json = (await response.json()) as StripeApiError & { + amount: number; + currency: string; + id: string; + }; + + if (!response.ok) { + throw new Error(json.error?.message ?? "Stripe transfer creation failed"); + } + + return json; +} diff --git a/apps/marketplace/src/lib/stripe/process-ledger-stripe-webhook.ts b/apps/marketplace/src/lib/stripe/process-ledger-stripe-webhook.ts new file mode 100644 index 00000000..e736d53d --- /dev/null +++ b/apps/marketplace/src/lib/stripe/process-ledger-stripe-webhook.ts @@ -0,0 +1,180 @@ +import type { Pool } from "pg"; + +import { + insertPlatformBalanceSnapshot, + markStripeWebhookProcessed, + tryInsertStripeWebhookEvent, + updateLedgerSettlementForCharge, + updateStripeTransferRowStatus, +} from "@/lib/ledger/repository"; +import { + getStripePlatformBalance, + retrieveStripeBalanceTransaction, +} from "@/lib/stripe/payout-api"; + +export type StripeWebhookEventEnvelope = { + data?: { object?: Record }; + id: string; + livemode: boolean; + type: string; +}; + +const LEDGER_EVENT_TYPES = new Set([ + "charge.succeeded", + "balance.available", + "transfer.created", + "transfer.updated", + "transfer.reversed", +]); + +export function isLedgerStripeEventType(type: string): boolean { + return LEDGER_EVENT_TYPES.has(type); +} + +export async function tryRecordStripeWebhookInbox( + pool: Pool, + payloadRaw: string, + event: StripeWebhookEventEnvelope, +): Promise { + let payloadJson: unknown; + + try { + payloadJson = JSON.parse(payloadRaw) as unknown; + } catch { + payloadJson = { parseError: true }; + } + + const row = await tryInsertStripeWebhookEvent(pool, { + eventType: event.type, + livemode: event.livemode, + payloadJson, + payloadRaw, + stripeEventId: event.id, + }); + + return row !== null; +} + +export async function processLedgerStripeSideEffects( + pool: Pool, + event: StripeWebhookEventEnvelope, +): Promise { + switch (event.type) { + case "charge.succeeded": + await handleChargeSucceeded(pool, event.data?.object); + break; + case "balance.available": + await handleBalanceAvailable(pool); + break; + case "transfer.created": + case "transfer.updated": + case "transfer.reversed": + await handleTransferEvent(pool, event.data?.object); + break; + default: + break; + } +} + +async function handleChargeSucceeded( + pool: Pool, + charge: Record | undefined, +): Promise { + if (!charge?.id || typeof charge.id !== "string") { + return; + } + + const chargeId = charge.id; + let balanceTransactionId: string | null = null; + let availableOn: Date | null = null; + + const bt = charge.balance_transaction; + + if (typeof bt === "string") { + balanceTransactionId = bt; + + try { + const full = await retrieveStripeBalanceTransaction(bt); + + availableOn = new Date(full.available_on * 1000); + } catch (error) { + console.error("[ledger] balance transaction fetch failed", error); + } + } else if (bt && typeof bt === "object") { + const obj = bt as { available_on?: number; id?: string }; + + balanceTransactionId = + typeof obj.id === "string" ? obj.id : balanceTransactionId; + + if (typeof obj.available_on === "number") { + availableOn = new Date(obj.available_on * 1000); + } + } + + await updateLedgerSettlementForCharge(pool, { + availableOn, + balanceTransactionId, + stripeChargeId: chargeId, + }); +} + +async function handleBalanceAvailable(pool: Pool): Promise { + const bal = await getStripePlatformBalance(); + const currencies = new Set(); + + for (const row of bal.available) { + currencies.add(row.currency.toLowerCase()); + } + + for (const row of bal.pending) { + currencies.add(row.currency.toLowerCase()); + } + + for (const currency of currencies) { + const availableMinor = BigInt( + bal.available + .filter((r) => r.currency.toLowerCase() === currency) + .reduce((sum, r) => sum + r.amount, 0), + ); + const pendingMinor = BigInt( + bal.pending + .filter((r) => r.currency.toLowerCase() === currency) + .reduce((sum, r) => sum + r.amount, 0), + ); + + await insertPlatformBalanceSnapshot(pool, { + availableMinor, + currency, + pendingMinor, + sourceType: "balance.available", + }); + } +} + +async function handleTransferEvent( + pool: Pool, + obj: Record | undefined, +): Promise { + const id = typeof obj?.id === "string" ? obj.id : null; + const status = typeof obj?.status === "string" ? obj.status : null; + + if (!id || !status) { + return; + } + + await updateStripeTransferRowStatus(pool, { + stripeStatus: status, + stripeTransferId: id, + }); +} + +export async function finalizeStripeWebhookInbox( + pool: Pool, + eventId: string, + result: string, +): Promise { + await markStripeWebhookProcessed(pool, { + result, + stripeEventId: eventId, + }); +} diff --git a/apps/marketplace/src/providers/auth-provider.tsx b/apps/marketplace/src/providers/auth-provider.tsx index 29c1912a..95bdbc5b 100644 --- a/apps/marketplace/src/providers/auth-provider.tsx +++ b/apps/marketplace/src/providers/auth-provider.tsx @@ -39,6 +39,11 @@ interface User { } interface AuthContextType { + /** + * JWT for same-origin API routes: vendor session cookie token or Saleor App Bridge token. + * Prefer `Authorization: Bearer` from the client when cookies are unreliable (iframe). + */ + apiAccessToken: string | null; /** True when opened from dashboard with saleorApiUrl in URL (uses app token) */ dashboardContext: boolean; isAuthenticated: boolean; @@ -761,6 +766,12 @@ export function AuthProvider({ // Authenticated: Saleor Cloud user token (App Bridge/login) OR dashboard context (saleorApiUrl in URL) const isAuthenticated = !!token || (dashboardContext && !!getAppBridgeDomain()); + const apiAccessToken = + typeof token === "string" && token.length > 0 + ? token + : typeof appBridgeToken === "string" && appBridgeToken.length > 0 + ? appBridgeToken + : null; const stripeConnectRequired = Boolean(user?.vendorId) && (!user?.stripePaymentAccountId || !user?.stripePaymentAccountConnected); @@ -768,6 +779,7 @@ export function AuthProvider({ return ( asOf) { + return false; + } + return true; +} diff --git a/packages/i18n/src/messages/en/marketplace.json b/packages/i18n/src/messages/en/marketplace.json index 1296d9e9..52ea2361 100644 --- a/packages/i18n/src/messages/en/marketplace.json +++ b/packages/i18n/src/messages/en/marketplace.json @@ -661,6 +661,82 @@ "vendor-name": "Vendor Name" }, "total-count": "{count} total" + }, + "payouts": { + "batch": { + "fees": "Fees", + "gross": "Gross", + "net": "Net", + "transfer-initiated": "Transfer initiated (UTC)", + "vendor-id": "Vendor ID" + }, + "batches-paid-title": "Processed batches", + "batches-pending-title": "Pending batches", + "close-period": { + "currency-label": "Currency (ISO)", + "description": "Lock a date range and currency: eligible available lines are summed per vendor into a batch. You can close the same period again later to include vendors or orders that were not in an earlier batch. Then run “Execute transfers” on each batch.", + "end-label": "Period end", + "error": "Could not close period.", + "error-422": "No eligible lines: need available funds (order_gross, not already in a batch) in this period.", + "start-label": "Period start", + "submit": "Close period & create batch", + "success": "Batch created. Expand the row to review lines, then execute transfers if ready.", + "title": "Close payout period" + }, + "sync-stripe": { + "busy": "Syncing…", + "description": "Re-fetch Stripe balance transactions for unconsumed order lines (unique charges, up to 500 per run). Then promote ledger rows to “available” when available_on is already in the past. Use if the ledger lags webhooks or before closing a period.", + "error": "Stripe sync failed.", + "partial": "{count, plural, one {# charge failed (see response).} other {# charges failed (see response).}}", + "submit": "Sync from Stripe", + "success": "Updated {charges} charge(s); {promoted} row(s) promoted by date.", + "title": "Refresh settlement from Stripe" + }, + "description": "Payout batches from the marketplace ledger: close a period, review vendor lines, run Stripe Connect transfers.", + "execute": { + "busy": "Executing…", + "button": "Execute Stripe transfers", + "error": "Execute failed.", + "hint": "Requires platform balance in Stripe and batch lines in status “ready”.", + "success": "Transfers submitted. Refreshing…" + }, + "failed-to-load": "Failed to load payout overview.", + "failed-to-load-batch": "Failed to load batch details.", + "ledgerLines": { + "availableUnknown": "Pending", + "colAmount": "Amount", + "colAvailable": "Funds available (UTC)", + "colBatch": "In payout batch", + "colCharge": "Stripe charge", + "colOccurred": "Occurred", + "colOrder": "Order", + "colOrderPlaced": "Order created", + "colStatus": "Funds status", + "colType": "Type", + "colVendor": "Vendor", + "description": "Per-order amounts still to pay out: pending Stripe settlement, available for the next period close, or held. Excludes refunded/reversed.", + "empty": "Nothing to pay out in this queue (or all lines are refunded/reversed, or already in a batch).", + "title": "Queued vendor payoffs" + }, + "no-batches": "No payout batches yet.", + "not-configured": "Ledger database is not configured. Set DATABASE_URL (or the ledger pool env) to enable this view.", + "open-from-dashboard": "Open the app from the Saleor dashboard to use this view.", + "sign-in-to-view": "Sign in to view payout data.", + "tab-label": "Payouts", + "table": { + "created": "Created", + "currency": "Currency", + "id": "Batch ID", + "period": "Period", + "status": "Status", + "transferInitiated": "First transfer (UTC)" + }, + "title": "Payout batches", + "toggle-details": "Show or hide batch line items", + "unauthorized": "You are not authorized to view payout data.", + "vendor-available": "Available (ledger)", + "vendor-pending": "Pending (Stripe)", + "vendor-summary-title": "Your vendor ledger (when signed in as vendor)" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 111db91c..2ab2e83a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -198,6 +198,9 @@ importers: nodemailer: specifier: 8.0.2 version: 8.0.2 + pg: + specifier: ^8.16.0 + version: 8.20.0 react: specifier: ^19.2.3 version: 19.2.4 @@ -220,6 +223,9 @@ importers: '@types/nodemailer': specifier: 7.0.11 version: 7.0.11 + '@types/pg': + specifier: ^8.15.5 + version: 8.15.6 '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -9262,10 +9268,21 @@ packages: peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.12.0: + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} + pg-pool@3.13.0: + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} + peerDependencies: + pg: '>=8.0' + pg-protocol@1.13.0: resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} @@ -9273,6 +9290,18 @@ packages: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} + pg@8.20.0: + resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -17985,7 +18014,7 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) @@ -18022,7 +18051,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -18052,14 +18081,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -18085,7 +18114,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -20823,8 +20852,17 @@ snapshots: peberminta@0.9.0: {} + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.12.0: {} + pg-int8@1.0.1: {} + pg-pool@3.13.0(pg@8.20.0): + dependencies: + pg: 8.20.0 + pg-protocol@1.13.0: {} pg-types@2.2.0: @@ -20835,6 +20873,20 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 + pg@8.20.0: + dependencies: + pg-connection-string: 2.12.0 + pg-pool: 3.13.0(pg@8.20.0) + pg-protocol: 1.13.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} diff --git a/turbo.json b/turbo.json index 208c31bb..26ea5864 100644 --- a/turbo.json +++ b/turbo.json @@ -34,13 +34,15 @@ "MARKETPLACE_SMTP_SECURE", "MARKETPLACE_EMAIL_FROM", "MARKETPLACE_SUPERADMIN_EMAIL", + "MARKETPLACE_STRIPE_CONNECT_WEBHOOK_SECRET", "NEXT_PUBLIC_SALEOR_URL", "NEXT_PUBLIC_MARKETPLACE_VENDOR_URL", "NEXT_PUBLIC_MARKETPLACE_STOREFRONT_URL", "NEXT_PUBLIC_SALEOR_MARKETPLACE_CHANNEL_SLUG", "NEXT_PUBLIC_GRAPHQL_URL", "NEXT_PUBLIC_SALEOR_UI_APP_TOKEN", - "ALLOWED_SALEOR_DOMAINS" + "ALLOWED_SALEOR_DOMAINS", + "DATABASE_URL" ], "remoteCache": { "enabled": true From ffda55681c434aeae7d48a93a5e405267b64ce42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bogda=C5=82?= Date: Fri, 17 Apr 2026 10:50:37 +0200 Subject: [PATCH 15/23] feat: add migrate command --- .env.example | 2 ++ AGENTS.md | 4 ++-- apps/marketplace/.env.example | 3 ++- apps/marketplace/package.json | 1 + package.json | 3 ++- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 640c3d5b..e6fe5aeb 100644 --- a/.env.example +++ b/.env.example @@ -73,3 +73,5 @@ MARKETPLACE_SUPERADMIN_EMAIL= # Stripe Connect (marketplace vendor onboarding; uses STRIPE_SECRET_KEY above) MARKETPLACE_STRIPE_CONNECT_WEBHOOK_SECRET= MARKETPLACE_STRIPE_CONNECT_DEFAULT_COUNTRY=US +# Ledger Postgres — required for `pnpm migrate:ledger` from repo root (dotenv loads this file). Also set the same value in apps/marketplace/.env for `next dev` / production. +# DATABASE_URL=postgresql://marketplace:marketplace@127.0.0.1:5434/marketplace_ledger diff --git a/AGENTS.md b/AGENTS.md index af5f7b06..c8932400 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,7 +95,7 @@ For decision tree and detailed scenarios, see `.agents/skills/project-guidelines ### Marketplace (`apps/marketplace`) - **Role:** Multi-vendor operations UI (products, orders, configuration) against Saleor via stitched GraphQL; vendors authenticate with JWT (`src/lib/auth/`, `src/providers/auth-provider.tsx`). -- **Ledger & payouts (Postgres):** Optional `DATABASE_URL`. Apply schema with `node apps/marketplace/scripts/migrate-ledger.mjs` (runs all `apps/marketplace/db/migrations/*.sql` in order). Core tables: `ledger_entries` (idempotent lines, `consumed_in_batch_id` ties lines to a closed batch), `payout_batches` / `payout_batch_items`, `stripe_transfers` (Stripe **Transfer** to Connect per batch line). Settlement stops at Transfers; **Stripe Payout** objects (bank withdrawal from Connect) are not persisted. +- **Ledger & payouts (Postgres):** Optional `DATABASE_URL`. Apply schema with `pnpm migrate:ledger` from repo root (`dotenv` loads root `.env`) or `pnpm migrate:ledger` inside `apps/marketplace` with `DATABASE_URL` set (runs all `apps/marketplace/db/migrations/*.sql` in order). Core tables: `ledger_entries` (idempotent lines, `consumed_in_batch_id` ties lines to a closed batch), `payout_batches` / `payout_batch_items`, `stripe_transfers` (Stripe **Transfer** to Connect per batch line). Settlement stops at Transfers; **Stripe Payout** objects (bank withdrawal from Connect) are not persisted. - **APIs:** Stripe Connect webhooks, payment/PI routes, Saleor webhooks (e.g. order-paid → ledger ingest), payout overview / close / execute batch — see `src/app/api/` and `src/lib/ledger/`. - **Storefront coupling:** Marketplace checkout flow uses vendor metadata and Stripe Connect; see storefront checkout/cart changes that reference marketplace payment URLs when configured. @@ -225,7 +225,7 @@ For decision tree and detailed scenarios, see `.agents/skills/project-guidelines - Storefront and Stripe use Sentry; config in `sentry.*.config.ts`. Error service in storefront sets user context on server and passes minimal user info to client for reporting. 5. **Marketplace ledger (Postgres)** - - Requires `DATABASE_URL` (see root / `apps/marketplace` `.env.example`). Apply migrations: `node apps/marketplace/scripts/migrate-ledger.mjs` from repo root with env loaded. Schema lives in `apps/marketplace/db/migrations/` (typically a single init file for greenfield; idempotent `create if not exists` where appropriate). + - Requires `DATABASE_URL` (see root / `apps/marketplace` `.env.example`). Apply migrations: `pnpm migrate:ledger` from repo root (or `pnpm --filter marketplace migrate:ledger`). Schema lives in `apps/marketplace/db/migrations/` (typically a single init file for greenfield; idempotent `create if not exists` where appropriate). --- diff --git a/apps/marketplace/.env.example b/apps/marketplace/.env.example index 72e408bc..433fb4fa 100644 --- a/apps/marketplace/.env.example +++ b/apps/marketplace/.env.example @@ -1,5 +1,6 @@ # Copy to `.env` and adjust. Many marketplace vars also live in the monorepo root `.env.example`. # --- Ledger / payout (local Postgres; see docker-compose.yml) --- -# After: `docker compose up -d` from this directory +# Same DATABASE_URL should exist in the repo root `.env` if you run `pnpm migrate:ledger` from the monorepo root. +# After: `pnpm db-ledger:up` from root (or `docker compose up -d db-ledger` in this directory) DATABASE_URL=postgresql://marketplace:marketplace@127.0.0.1:5434/marketplace_ledger diff --git a/apps/marketplace/package.json b/apps/marketplace/package.json index cb67d8c4..570664e5 100644 --- a/apps/marketplace/package.json +++ b/apps/marketplace/package.json @@ -68,6 +68,7 @@ "localstack:up": "docker-compose up -d localstack", "db-ledger:up": "docker-compose up -d db-ledger", "db-ledger:down": "docker-compose stop db-ledger", + "migrate:ledger": "node ./scripts/migrate-ledger.mjs", "start": "next start --port 3001", "test": "vitest run", "test:coverage": "vitest run --coverage", diff --git a/package.json b/package.json index 91053367..17ace6f3 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "localstack:down": "docker compose -f apps/marketplace/docker-compose.yml down", "localstack:export-secret": "docker compose -f apps/marketplace/docker-compose.yml exec localstack bash /opt/localstack/export-secret.sh", "db-ledger:up": "docker compose -f apps/marketplace/docker-compose.yml up -d db-ledger", - "db-ledger:down": "docker compose -f apps/marketplace/docker-compose.yml stop db-ledger" + "db-ledger:down": "docker compose -f apps/marketplace/docker-compose.yml stop db-ledger", + "migrate:ledger": "dotenv -- node apps/marketplace/scripts/migrate-ledger.mjs" }, "devDependencies": { "@nimara/config": "workspace:*", From 21b9122b46a626c88bb8508855d8976e3a731d06 Mon Sep 17 00:00:00 2001 From: Grzegorz Derdak Date: Wed, 22 Apr 2026 15:22:59 +0200 Subject: [PATCH 16/23] feat(marketplace): adjust cart and checkout view to marketplace use-case --- .../src/graphql/generated/client.ts | 813 +++++++++++------- apps/storefront/global.d.ts | 3 +- .../app/[locale]/(checkout)/checkout/page.tsx | 176 ++-- .../(main)/cart/_actions/cart-actions.ts | 26 +- .../_components/marketplace-cart-view.tsx | 62 +- .../src/app/[locale]/(main)/cart/page.tsx | 54 +- .../(main)/payment/confirmation/page.tsx | 4 +- .../products/[slug]/_actions/add-to-bag.ts | 4 +- apps/storefront/src/config.ts | 3 +- apps/storefront/src/features/checkout/cart.ts | 4 +- .../src/features/checkout/checkout-actions.ts | 17 +- .../src/features/checkout/summary.tsx | 11 + .../storefront/src/features/header/header.tsx | 4 +- apps/storefront/src/foundation/auth/login.ts | 4 +- .../actions/update-checkout-address-action.ts | 4 +- .../checkout/sections/checkout-section.tsx | 6 +- .../delivery-method/marketplace-form.tsx | 17 +- .../sections/delivery-method/section.tsx | 18 +- .../checkout/sections/payment/payment.tsx | 3 +- .../foundation/checkout/sections/sections.tsx | 77 +- .../sections/shipping-address/section.tsx | 2 +- .../checkout/sections/user-details/actions.ts | 4 +- .../sections/user-details/section.tsx | 2 +- .../src/services/lazy-loaders/marketplace.ts | 35 + apps/storefront/src/services/marketplace.ts | 7 + apps/storefront/src/services/registry.ts | 3 + apps/stripe/src/graphql/generated/client.ts | 793 ++++++++++------- packages/codegen/schema.ts | 799 ++++++++++------- packages/domain/src/objects/Marketplace.ts | 10 + .../cart/shared/components/cart-details.tsx | 11 + .../src/cart/shared/components/cart-shell.tsx | 9 + .../src/cart/shop-basic-cart/standard.tsx | 53 +- .../shop-basic-pdp/standard.tsx | 1 + .../shared/shopping-bag/components/line.tsx | 16 +- .../shared/shopping-bag/components/lines.tsx | 102 ++- .../src/shared/shopping-bag/helpers.ts | 53 ++ .../src/form-components/radio-form-group.tsx | 2 +- .../acp/saleor/graphql/fragments/generated.ts | 6 +- .../acp/saleor/graphql/mutations/generated.ts | 2 +- .../acp/saleor/graphql/queries/generated.ts | 6 +- .../saleor/graphql/fragments/generated.ts | 2 +- .../cart/saleor/graphql/queries/generated.ts | 2 +- .../saleor/graphql/fragments/generated.ts | 5 +- .../saleor/graphql/queries/generated.ts | 2 +- .../saleor/graphql/fragments/generated.ts | 2 +- .../saleor/graphql/queries/generated.ts | 2 +- .../saleor/graphql/queries/generated.ts | 2 +- .../src/graphql/fragments/generated.ts | 2 +- .../src/graphql/mutations/generated.ts | 8 +- .../graphql/fragments/VendorProfile.graphql | 12 + .../saleor/graphql/fragments/generated.ts | 41 + .../graphql/queries/VendorGetByID.graphql | 5 + .../graphql/queries/VendorGetBySlug.graphql | 5 + .../saleor/graphql/queries/generated.ts | 86 ++ .../src/marketplace/saleor/serializers.ts | 12 + .../src/marketplace/saleor/service.ts | 86 ++ .../infrastructure/src/marketplace/types.ts | 21 + .../saleor/graphql/queries/generated.ts | 2 +- .../saleor/graphql/fragments/generated.ts | 6 +- .../store/saleor/graphql/queries/generated.ts | 6 +- packages/infrastructure/src/types.ts | 7 + .../saleor/graphql/fragments/generated.ts | 2 +- .../user/saleor/graphql/queries/generated.ts | 2 +- 63 files changed, 2254 insertions(+), 1292 deletions(-) create mode 100644 apps/storefront/src/services/lazy-loaders/marketplace.ts create mode 100644 apps/storefront/src/services/marketplace.ts create mode 100644 packages/domain/src/objects/Marketplace.ts create mode 100644 packages/features/src/cart/shared/components/cart-shell.tsx create mode 100644 packages/features/src/shared/shopping-bag/helpers.ts create mode 100644 packages/infrastructure/src/marketplace/saleor/graphql/fragments/VendorProfile.graphql create mode 100644 packages/infrastructure/src/marketplace/saleor/graphql/fragments/generated.ts create mode 100644 packages/infrastructure/src/marketplace/saleor/graphql/queries/VendorGetByID.graphql create mode 100644 packages/infrastructure/src/marketplace/saleor/graphql/queries/VendorGetBySlug.graphql create mode 100644 packages/infrastructure/src/marketplace/saleor/graphql/queries/generated.ts create mode 100644 packages/infrastructure/src/marketplace/saleor/serializers.ts create mode 100644 packages/infrastructure/src/marketplace/saleor/service.ts create mode 100644 packages/infrastructure/src/marketplace/types.ts diff --git a/apps/marketplace/src/graphql/generated/client.ts b/apps/marketplace/src/graphql/generated/client.ts index bd80286b..78c7f097 100644 --- a/apps/marketplace/src/graphql/generated/client.ts +++ b/apps/marketplace/src/graphql/generated/client.ts @@ -255,8 +255,8 @@ export type AccountErrorCode = | 'DELETE_OWN_ACCOUNT' | 'DELETE_STAFF_ACCOUNT' | 'DELETE_SUPERUSER_ACCOUNT' - | 'DISABLED_AUTHENTICATION_METHOD' | 'DUPLICATED_INPUT_ITEM' + | 'FILE_SIZE_LIMIT_EXCEEDED' | 'GRAPHQL_ERROR' | 'INACTIVE' | 'INVALID' @@ -1046,21 +1046,38 @@ export type AppExtension = Node & { /** Label of the extension to show in the dashboard. */ label: Scalars['String']['output']; /** - * Name of the extension mount point in the dashboard. Value returned in UPPERCASE. + * Place where given extension will be mounted. + * @deprecated Use `mountName` instead. + */ + mount: AppExtensionMountEnum; + /** + * Name of the extension mount point in the dashboard. Replaces `mount` * * Added in Saleor 3.22. */ mountName: Scalars['String']['output']; + /** + * App extension options. + * + * Added in Saleor 3.22. + * @deprecated Use `settings` field instead. + */ + options: Maybe; /** List of the app extension's permissions. */ permissions: Array; /** - * App extension settings. + * App extension settings. Replaces `options` field. * * Added in Saleor 3.22. */ settings: Scalars['JSON']['output']; /** - * Name of the extension target in the dashboard. Value returned in UPPERCASE. + * Type of way how app extension will be opened. + * @deprecated Use `targetName` instead. + */ + target: AppExtensionTargetEnum; + /** + * Name of the extension target in the dashboard. Replaces `target` * * Added in Saleor 3.22. */ @@ -1085,12 +1102,24 @@ export type AppExtensionCountableEdge = { }; export type AppExtensionFilterInput = { + /** + * DEPRECATED: Use `mountName` instead. + * + * DEPRECATED: this field will be removed. + */ + mount?: InputMaybe>; /** * Plain-text mount name (case insensitive) * * Added in Saleor 3.22. */ mountName?: InputMaybe>; + /** + * DEPRECATED: Use `targetName` instead. + * + * DEPRECATED: this field will be removed. + */ + target?: InputMaybe; /** * Plain-text target name (case insensitive) * @@ -1099,6 +1128,92 @@ export type AppExtensionFilterInput = { targetName?: InputMaybe; }; +/** All places where app extension can be mounted. */ +export type AppExtensionMountEnum = + | 'CATEGORY_DETAILS_MORE_ACTIONS' + | 'CATEGORY_OVERVIEW_CREATE' + | 'CATEGORY_OVERVIEW_MORE_ACTIONS' + | 'COLLECTION_DETAILS_MORE_ACTIONS' + | 'COLLECTION_DETAILS_WIDGETS' + | 'COLLECTION_OVERVIEW_CREATE' + | 'COLLECTION_OVERVIEW_MORE_ACTIONS' + | 'CUSTOMER_DETAILS_MORE_ACTIONS' + | 'CUSTOMER_DETAILS_WIDGETS' + | 'CUSTOMER_OVERVIEW_CREATE' + | 'CUSTOMER_OVERVIEW_MORE_ACTIONS' + | 'DISCOUNT_DETAILS_MORE_ACTIONS' + | 'DISCOUNT_OVERVIEW_CREATE' + | 'DISCOUNT_OVERVIEW_MORE_ACTIONS' + | 'DRAFT_ORDER_DETAILS_MORE_ACTIONS' + | 'DRAFT_ORDER_DETAILS_WIDGETS' + | 'DRAFT_ORDER_OVERVIEW_CREATE' + | 'DRAFT_ORDER_OVERVIEW_MORE_ACTIONS' + | 'GIFT_CARD_DETAILS_MORE_ACTIONS' + | 'GIFT_CARD_DETAILS_WIDGETS' + | 'GIFT_CARD_OVERVIEW_CREATE' + | 'GIFT_CARD_OVERVIEW_MORE_ACTIONS' + | 'MENU_DETAILS_MORE_ACTIONS' + | 'MENU_OVERVIEW_CREATE' + | 'MENU_OVERVIEW_MORE_ACTIONS' + | 'NAVIGATION_CATALOG' + | 'NAVIGATION_CUSTOMERS' + | 'NAVIGATION_DISCOUNTS' + | 'NAVIGATION_ORDERS' + | 'NAVIGATION_PAGES' + | 'NAVIGATION_TRANSLATIONS' + | 'ORDER_DETAILS_MORE_ACTIONS' + | 'ORDER_DETAILS_WIDGETS' + | 'ORDER_OVERVIEW_CREATE' + | 'ORDER_OVERVIEW_MORE_ACTIONS' + | 'PAGE_DETAILS_MORE_ACTIONS' + | 'PAGE_OVERVIEW_CREATE' + | 'PAGE_OVERVIEW_MORE_ACTIONS' + | 'PAGE_TYPE_DETAILS_MORE_ACTIONS' + | 'PAGE_TYPE_OVERVIEW_CREATE' + | 'PAGE_TYPE_OVERVIEW_MORE_ACTIONS' + | 'PRODUCT_DETAILS_MORE_ACTIONS' + | 'PRODUCT_DETAILS_WIDGETS' + | 'PRODUCT_OVERVIEW_CREATE' + | 'PRODUCT_OVERVIEW_MORE_ACTIONS' + | 'TRANSLATIONS_MORE_ACTIONS' + | 'VOUCHER_DETAILS_MORE_ACTIONS' + | 'VOUCHER_DETAILS_WIDGETS' + | 'VOUCHER_OVERVIEW_CREATE' + | 'VOUCHER_OVERVIEW_MORE_ACTIONS'; + +/** Represents the options for an app extension. */ +export type AppExtensionOptionsNewTab = { + /** + * Options controlling behavior of the NEW_TAB extension target + * @deprecated Use `settings` field directly. + */ + newTabTarget: Maybe; +}; + +/** Represents the options for an app extension. */ +export type AppExtensionOptionsWidget = { + /** + * Options for displaying a Widget + * @deprecated Use `settings` field directly. + */ + widgetTarget: Maybe; +}; + +export type AppExtensionPossibleOptions = AppExtensionOptionsNewTab | AppExtensionOptionsWidget; + +/** + * All available ways of opening an app extension. + * + * POPUP - app's extension will be mounted as a popup window + * APP_PAGE - redirect to app's page + * + */ +export type AppExtensionTargetEnum = + | 'APP_PAGE' + | 'NEW_TAB' + | 'POPUP' + | 'WIDGET'; + /** * Fetch and validate manifest. * @@ -1143,9 +1258,9 @@ export type AppInstallInput = { /** Determine if app will be set active or not. */ activateAfterInstallation?: InputMaybe; /** Name of the app to install. */ - appName: Scalars['String']['input']; + appName?: InputMaybe; /** URL to app's manifest in JSON format. */ - manifestUrl: Scalars['String']['input']; + manifestUrl?: InputMaybe; /** List of permission code names to assign to this app. */ permissions?: InputMaybe>; }; @@ -1207,7 +1322,12 @@ export type AppManifestExtension = { /** Label of the extension to show in the dashboard. */ label: Scalars['String']['output']; /** - * Name of the extension mount point in the dashboard. Value returned in UPPERCASE. + * Place where given extension will be mounted. + * @deprecated Use `mountName` instead. + */ + mount: AppExtensionMountEnum; + /** + * Name of the extension mount point in the dashboard. Replaces `mount` * * Added in Saleor 3.22. */ @@ -1215,13 +1335,18 @@ export type AppManifestExtension = { /** List of the app extension's permissions. */ permissions: Array; /** - * App extension settings. + * JSON object with settings for this extension. * * Added in Saleor 3.22. */ settings: Scalars['JSON']['output']; /** - * Name of the extension target in the dashboard. Value returned in UPPERCASE. + * Type of way how app extension will be opened. + * @deprecated Use `targetName` instead. + */ + target: AppExtensionTargetEnum; + /** + * Name of the extension target in the dashboard. Replaces `target` * * Added in Saleor 3.22. */ @@ -2064,7 +2189,7 @@ export type Attribute = Node & ObjectWithMetadata & { /** Public metadata. Use `keys` to control which fields you want to include. The default is to include everything. */ metafields: Maybe; /** Name of an attribute displayed in the interface. */ - name: Scalars['String']['output']; + name: Maybe; /** List of private metadata items. Requires staff permissions to access. */ privateMetadata: Array; /** @@ -2086,7 +2211,7 @@ export type Attribute = Node & ObjectWithMetadata & { */ referenceTypes: Maybe>; /** Internal representation of an attribute name. */ - slug: Scalars['String']['output']; + slug: Maybe; /** * The position of the attribute in the storefront navigation (0 by default). Requires one of the following permissions: MANAGE_PAGES, MANAGE_PAGE_TYPES_AND_ATTRIBUTES, MANAGE_PRODUCTS, MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES. * @deprecated No longer supported @@ -2095,7 +2220,7 @@ export type Attribute = Node & ObjectWithMetadata & { /** Returns translated attribute fields for the given language code. */ translation: Maybe; /** The attribute type. */ - type: AttributeTypeEnum; + type: Maybe; /** The unit of attribute values. */ unit: Maybe; /** Whether the attribute requires values to be passed or not. Requires one of the following permissions: MANAGE_PAGES, MANAGE_PAGE_TYPES_AND_ATTRIBUTES, MANAGE_PRODUCTS, MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES. */ @@ -4230,19 +4355,12 @@ export type Checkout = Node & ObjectWithMetadata & { * Added in Saleor 3.21. */ customerNote: Scalars['String']['output']; - /** - * The delivery method selected for this checkout. - * - * Added in Saleor 3.23. - */ - delivery: Maybe; /** * The delivery method selected for this checkout. * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Optionally triggered when cached external shipping methods are invalid. * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. - * @deprecated Use `delivery` instead. */ deliveryMethod: Maybe; /** The total discount applied to the checkout. Note: Only discount created via voucher are included in this field. */ @@ -4302,7 +4420,7 @@ export type Checkout = Node & ObjectWithMetadata & { * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Optionally triggered when cached external shipping methods are invalid. * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. - * @deprecated Use `delivery` instead. + * @deprecated Use `deliveryMethod` instead. */ shippingMethod: Maybe; /** @@ -4708,7 +4826,6 @@ export type CheckoutCustomerNoteUpdate = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ export type CheckoutDeliveryMethodUpdate = { @@ -5107,25 +5224,7 @@ export type CheckoutPaymentCreate = { }; /** Represents an problem in the checkout. */ -export type CheckoutProblem = CheckoutLineProblemInsufficientStock | CheckoutLineProblemVariantNotAvailable | CheckoutProblemDeliveryMethodInvalid | CheckoutProblemDeliveryMethodStale; - -/** - * Indicates that the selected delivery method is invalid. - * - * Added in Saleor 3.23. - */ -export type CheckoutProblemDeliveryMethodInvalid = { - delivery: Delivery; -}; - -/** - * Indicates that the delivery methods are stale. - * - * Added in Saleor 3.23. - */ -export type CheckoutProblemDeliveryMethodStale = { - delivery: Delivery; -}; +export type CheckoutProblem = CheckoutLineProblemInsufficientStock | CheckoutLineProblemVariantNotAvailable; /** * Remove a gift card or a voucher from a checkout. @@ -5143,12 +5242,6 @@ export type CheckoutRemovePromoCode = { /** Represents the channel-specific checkout settings. */ export type CheckoutSettings = { - /** - * Default to `true`. Determines whether gift cards can be attached to a Checkout via `addPromoCode` mutation. Usage of this mutation with gift cards is deprecated. - * - * Added in Saleor 3.23. - */ - allowLegacyGiftCardUse: Scalars['Boolean']['output']; /** * The date time defines the earliest checkout creation date on which fully paid checkouts can begin to be automatically completed. * @@ -5176,12 +5269,6 @@ export type CheckoutSettings = { }; export type CheckoutSettingsInput = { - /** - * Default to `true`. Determines whether gift cards can be attached to a Checkout via `addPromoCode` mutation. Usage of this mutation with gift cards is deprecated. - * - * Added in Saleor 3.23. - */ - allowLegacyGiftCardUse?: InputMaybe; /** * Settings for automatic completion of fully paid checkouts. * @@ -5223,7 +5310,6 @@ export type CheckoutShippingAddressUpdate = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout shipping method with the external one. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ export type CheckoutShippingMethodUpdate = { @@ -5240,9 +5326,7 @@ export type CheckoutSortField = /** Sort checkouts by customer. */ | 'CUSTOMER' /** Sort checkouts by payment. */ - | 'PAYMENT' - /** Sort checkouts by rank. Note: This option is available only with the `search` filter. */ - | 'RANK'; + | 'PAYMENT'; export type CheckoutSortingInput = { /** Specifies the direction in which to sort checkouts. */ @@ -5609,6 +5693,7 @@ export type CollectionError = { export type CollectionErrorCode = | 'CANNOT_MANAGE_PRODUCT_WITHOUT_VARIANT' | 'DUPLICATED_INPUT_ITEM' + | 'FILE_SIZE_LIMIT_EXCEEDED' | 'GRAPHQL_ERROR' | 'INVALID' | 'NOT_FOUND' @@ -6666,6 +6751,8 @@ export type CustomerEvent = Node & { message: Maybe; /** The concerned order. */ order: Maybe; + /** The concerned order line. */ + orderLine: Maybe; /** Customer event type. */ type: Maybe; /** User who performed the action. */ @@ -6931,49 +7018,207 @@ export type DeletePrivateMetadata = { metadataErrors: Array; }; +/** Represents a delivery method chosen for the checkout. `Warehouse` type is used when checkout is marked as "click and collect" and `ShippingMethod` otherwise. */ +export type DeliveryMethod = ShippingMethod | Warehouse; + +/** Represents digital content associated with a product variant. */ +export type DigitalContent = Node & ObjectWithMetadata & { + /** Indicator for automatic fulfillment of digital content. */ + automaticFulfillment: Scalars['Boolean']['output']; + /** File associated with digital content. */ + contentFile: Scalars['String']['output']; + /** The ID of the digital content. */ + id: Scalars['ID']['output']; + /** Maximum number of allowed downloads for the digital content. */ + maxDownloads: Maybe; + /** List of public metadata items. Can be accessed without permissions. */ + metadata: Array; + /** + * A single key from public metadata. + * + * Tip: Use GraphQL aliases to fetch multiple keys. + */ + metafield: Maybe; + /** Public metadata. Use `keys` to control which fields you want to include. The default is to include everything. */ + metafields: Maybe; + /** List of private metadata items. Requires staff permissions to access. */ + privateMetadata: Array; + /** + * A single key from private metadata. Requires staff permissions to access. + * + * Tip: Use GraphQL aliases to fetch multiple keys. + */ + privateMetafield: Maybe; + /** Private metadata. Requires staff permissions to access. Use `keys` to control which fields you want to include. The default is to include everything. */ + privateMetafields: Maybe; + /** Product variant assigned to digital content. */ + productVariant: ProductVariant; + /** Number of days the URL for the digital content remains valid. */ + urlValidDays: Maybe; + /** List of URLs for the digital variant. */ + urls: Maybe>; + /** Default settings indicator for digital content. */ + useDefaultSettings: Scalars['Boolean']['output']; +}; + + +/** Represents digital content associated with a product variant. */ +export type DigitalContentMetafieldArgs = { + key: Scalars['String']['input']; +}; + + +/** Represents digital content associated with a product variant. */ +export type DigitalContentMetafieldsArgs = { + keys?: InputMaybe>; +}; + + +/** Represents digital content associated with a product variant. */ +export type DigitalContentPrivateMetafieldArgs = { + key: Scalars['String']['input']; +}; + + +/** Represents digital content associated with a product variant. */ +export type DigitalContentPrivateMetafieldsArgs = { + keys?: InputMaybe>; +}; + +/** A connection to a list of digital content items. */ +export type DigitalContentCountableConnection = { + edges: Array; + /** Pagination data for this connection. */ + pageInfo: PageInfo; + /** A total count of items in the collection. */ + totalCount: Maybe; +}; + +export type DigitalContentCountableEdge = { + /** A cursor for use in pagination. */ + cursor: Scalars['String']['output']; + /** The item at the end of the edge. */ + node: DigitalContent; +}; + /** - * Represents a delivery option for the checkout. + * Create new digital content. This mutation must be sent as a `multipart` request. More detailed specs of the upload format can be found here: https://github.com/jaydenseric/graphql-multipart-request-spec * - * Added in Saleor 3.23. + * Requires one of the following permissions: MANAGE_PRODUCTS. */ -export type Delivery = { - /** The ID of the delivery. */ - id: Scalars['ID']['output']; - /** Shipping method represented by the delivery. */ - shippingMethod: Maybe; +export type DigitalContentCreate = { + content: Maybe; + errors: Array; + /** @deprecated Use `errors` field instead. */ + productErrors: Array; + variant: Maybe; }; -/** Represents a delivery method chosen for the checkout. `Warehouse` type is used when checkout is marked as "click and collect" and `ShippingMethod` otherwise. */ -export type DeliveryMethod = ShippingMethod | Warehouse; - /** - * Calculates available delivery options for a checkout. + * Remove digital content assigned to given variant. * - * Added in Saleor 3.23. + * Requires one of the following permissions: MANAGE_PRODUCTS. + */ +export type DigitalContentDelete = { + errors: Array; + /** @deprecated Use `errors` field instead. */ + productErrors: Array; + variant: Maybe; +}; + +export type DigitalContentInput = { + /** Overwrite default automatic_fulfillment setting for variant. */ + automaticFulfillment?: InputMaybe; + /** Determines how many times a download link can be accessed by a customer. */ + maxDownloads?: InputMaybe; + /** + * Fields required to update the digital content metadata. Can be read by any API client authorized to read the object it's attached to. + * + * Warning: never store sensitive information, including financial data such as credit card details. + */ + metadata?: InputMaybe>; + /** + * Fields required to update the digital content private metadata. Requires permissions to modify and to read the metadata of the object it's attached to. + * + * Warning: never store sensitive information, including financial data such as credit card details. + */ + privateMetadata?: InputMaybe>; + /** Determines for how many days a download link is active since it was generated. */ + urlValidDays?: InputMaybe; + /** Use default digital content settings for this product. */ + useDefaultSettings: Scalars['Boolean']['input']; +}; + +/** + * Updates digital content. * - * Triggers the following webhook events: - * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered to fetch external shipping methods. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Triggered to filter shipping methods. + * Requires one of the following permissions: MANAGE_PRODUCTS. */ -export type DeliveryOptionsCalculate = { - /** List of the available deliveries. */ - deliveries: Array; - errors: Array; +export type DigitalContentUpdate = { + content: Maybe; + errors: Array; + /** @deprecated Use `errors` field instead. */ + productErrors: Array; + variant: Maybe; }; -export type DeliveryOptionsCalculateError = { - /** The error code. */ - code: DeliveryOptionsCalculateErrorCode; - /** Name of a field that caused the error. A value of `null` indicates that the error isn't associated with a particular field. */ - field: Maybe; - /** The error message. */ - message: Maybe; +export type DigitalContentUploadInput = { + /** Overwrite default automatic_fulfillment setting for variant. */ + automaticFulfillment?: InputMaybe; + /** Represents an file in a multipart request. */ + contentFile: Scalars['Upload']['input']; + /** Determines how many times a download link can be accessed by a customer. */ + maxDownloads?: InputMaybe; + /** + * Fields required to update the digital content metadata. Can be read by any API client authorized to read the object it's attached to. + * + * Warning: never store sensitive information, including financial data such as credit card details. + */ + metadata?: InputMaybe>; + /** + * Fields required to update the digital content private metadata. Requires permissions to modify and to read the metadata of the object it's attached to. + * + * Warning: never store sensitive information, including financial data such as credit card details. + */ + privateMetadata?: InputMaybe>; + /** Determines for how many days a download link is active since it was generated. */ + urlValidDays?: InputMaybe; + /** Use default digital content settings for this product. */ + useDefaultSettings: Scalars['Boolean']['input']; }; -export type DeliveryOptionsCalculateErrorCode = - | 'GRAPHQL_ERROR' - | 'INVALID' - | 'NOT_FOUND'; +/** Represents a URL for digital content. */ +export type DigitalContentUrl = Node & { + /** Digital content associated with the URL. */ + content: DigitalContent; + /** Date and time when the digital content URL was created. */ + created: Scalars['DateTime']['output']; + /** Number of times digital content has been downloaded. */ + downloadNum: Scalars['Int']['output']; + /** The ID of the digital content URL. */ + id: Scalars['ID']['output']; + /** UUID of digital content. */ + token: Scalars['UUID']['output']; + /** URL for digital content. */ + url: Maybe; +}; + +/** + * Generate new URL to digital content. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + */ +export type DigitalContentUrlCreate = { + digitalContentUrl: Maybe; + errors: Array; + /** @deprecated Use `errors` field instead. */ + productErrors: Array; +}; + +export type DigitalContentUrlCreateInput = { + /** Digital content ID which URL will belong to. */ + content: Scalars['ID']['input']; +}; export type DiscountError = { /** List of channels IDs which causes the error. */ @@ -7144,11 +7389,7 @@ export type DraftOrderCreateInput = { user?: InputMaybe; /** Email address of the customer. */ userEmail?: InputMaybe; - /** - * ID of the voucher associated with the order. - * - * DEPRECATED: this field will be removed. Use `voucherCode` instead. - */ + /** ID of the voucher associated with the order. */ voucher?: InputMaybe; /** * A code of the voucher associated with the order. @@ -7257,11 +7498,7 @@ export type DraftOrderInput = { user?: InputMaybe; /** Email address of the customer. */ userEmail?: InputMaybe; - /** - * ID of the voucher associated with the order. - * - * DEPRECATED: this field will be removed. Use `voucherCode` instead. - */ + /** ID of the voucher associated with the order. */ voucher?: InputMaybe; /** * A code of the voucher associated with the order. @@ -7675,6 +7912,8 @@ export type ExportScope = * * Added in Saleor 3.18. * + * Note: this API is currently in Feature Preview and can be subject to changes at later point. + * * Requires one of the following permissions: MANAGE_DISCOUNTS. * * Triggers the following webhook events: @@ -8155,7 +8394,7 @@ export type GiftCard = Node & ObjectWithMetadata & { */ endDate: Maybe; /** - * List of events associated with the gift card. Requires MANAGE_GIFT_CARD permission to access all events. Users with MANAGE_ORDERS permission can access only USED_IN_ORDER and REFUNDED_IN_ORDER events. + * List of events associated with the gift card. Requires MANAGE_GIFT_CARD permission to access all events. Users with MANAGE_ORDERS permission can access only USED_IN_ORDER events. * * Requires one of the following permissions: MANAGE_GIFT_CARD, MANAGE_ORDERS. */ @@ -8577,7 +8816,6 @@ export type GiftCardEventsEnum = | 'EXPIRY_DATE_UPDATED' | 'ISSUED' | 'NOTE_ADDED' - | 'REFUNDED_IN_ORDER' | 'RESENT' | 'SENT_TO_CUSTOMER' | 'TAGS_UPDATED' @@ -8626,55 +8864,6 @@ export type GiftCardMetadataUpdated = Event & { version: Maybe; }; -/** - * Represents a gift card payment method used for a transaction. - * - * Added in Saleor 3.23. - */ -export type GiftCardPaymentMethodDetails = PaymentMethodDetails & { - /** - * Brand of the gift card. - * - * Added in Saleor 3.23. - */ - brand: Maybe; - /** - * Indicates whether the gift card is a built-in Saleor gift card. - * - * Added in Saleor 3.23. - */ - isSaleorGiftcard: Scalars['Boolean']['output']; - /** - * Last characters of the gift card code. Max 4 characters. - * - * Added in Saleor 3.23. - */ - lastChars: Maybe; - /** Name of the gift card. */ - name: Scalars['String']['output']; -}; - -export type GiftCardPaymentMethodDetailsInput = { - /** - * Brand of the gift card used for the transaction. Max length is 40 characters. - * - * Added in Saleor 3.23. - */ - brand?: InputMaybe; - /** - * Last characters of the gift card used for the transaction. Max length is 4 characters. - * - * Added in Saleor 3.23. - */ - lastChars?: InputMaybe; - /** - * Name of the payment method used for the transaction. Max length is 256 characters. - * - * Added in Saleor 3.23. - */ - name: Scalars['String']['input']; -}; - /** * Resend a gift card. * @@ -8767,8 +8956,6 @@ export type GiftCardSortField = | 'CURRENT_BALANCE' /** Sort gift cards by product. */ | 'PRODUCT' - /** Sort gift cards by rank. Note: This option is available only with the `search` filter. */ - | 'RANK' /** Sort gift cards by used by. */ | 'USED_BY'; @@ -8933,6 +9120,10 @@ export type GroupCountableEdge = { node: Group; }; +export type HttpMethod = + | 'GET' + | 'POST'; + /** Thumbnail formats for icon images. */ export type IconThumbnailFormatEnum = | 'ORIGINAL' @@ -12156,7 +12347,6 @@ export type Mutation = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ checkoutDeliveryMethodUpdate: Maybe; @@ -12224,7 +12414,6 @@ export type Mutation = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout shipping method with the external one. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. * @deprecated Use `checkoutDeliveryMethodUpdate` instead. */ @@ -12368,15 +12557,33 @@ export type Mutation = { */ deleteWarehouse: Maybe; /** - * Calculates available delivery options for a checkout. + * Create new digital content. This mutation must be sent as a `multipart` request. More detailed specs of the upload format can be found here: https://github.com/jaydenseric/graphql-multipart-request-spec * - * Added in Saleor 3.23. + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContentCreate: Maybe; + /** + * Remove digital content assigned to given variant. * - * Triggers the following webhook events: - * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered to fetch external shipping methods. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Triggered to filter shipping methods. + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. */ - deliveryOptionsCalculate: Maybe; + digitalContentDelete: Maybe; + /** + * Updates digital content. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContentUpdate: Maybe; + /** + * Generate new URL to digital content. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContentUrlCreate: Maybe; /** * Deletes draft orders. * @@ -12428,7 +12635,6 @@ export type Mutation = { * Triggers the following webhook events: * - NOTIFY_USER (async): A notification for the exported file. * - GIFT_CARD_EXPORT_COMPLETED (async): A notification for the exported file. - * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportGiftCards: Maybe; /** @@ -12439,7 +12645,6 @@ export type Mutation = { * Triggers the following webhook events: * - NOTIFY_USER (async): A notification for the exported file. * - PRODUCT_EXPORT_COMPLETED (async): A notification for the exported file. - * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportProducts: Maybe; /** @@ -12447,11 +12652,12 @@ export type Mutation = { * * Added in Saleor 3.18. * + * Note: this API is currently in Feature Preview and can be subject to changes at later point. + * * Requires one of the following permissions: MANAGE_DISCOUNTS. * * Triggers the following webhook events: * - VOUCHER_CODE_EXPORT_COMPLETED (async): A notification for the exported file. - * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportVoucherCodes: Maybe; /** Prepare external authentication URL for user by custom plugin. */ @@ -14553,8 +14759,25 @@ export type MutationDeleteWarehouseArgs = { }; -export type MutationDeliveryOptionsCalculateArgs = { - id: Scalars['ID']['input']; +export type MutationDigitalContentCreateArgs = { + input: DigitalContentUploadInput; + variantId: Scalars['ID']['input']; +}; + + +export type MutationDigitalContentDeleteArgs = { + variantId: Scalars['ID']['input']; +}; + + +export type MutationDigitalContentUpdateArgs = { + input: DigitalContentInput; + variantId: Scalars['ID']['input']; +}; + + +export type MutationDigitalContentUrlCreateArgs = { + input: DigitalContentUrlCreateInput; }; @@ -15911,6 +16134,15 @@ export type NavigationType = /** Secondary storefront navigation. */ | 'SECONDARY'; +/** Represents the NEW_TAB target options for an app extension. */ +export type NewTabTargetOptions = { + /** + * HTTP method for New Tab target (GET or POST) + * @deprecated Use `settings` field directly. + */ + method: HttpMethod; +}; + /** An object with an ID */ export type Node = { /** The ID of the object. */ @@ -17553,6 +17785,7 @@ export type OrderLine = Node & ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_PRODUCTS, MANAGE_ORDERS. */ allocations: Maybe>; + digitalContentUrl: Maybe; /** * List of applied discounts * @@ -18717,8 +18950,6 @@ export type PageSortField = | 'PUBLICATION_DATE' /** Sort pages by publication date. */ | 'PUBLISHED_AT' - /** Sort pages by rank. Note: This option is available only with the `search` filter. */ - | 'RANK' /** Sort pages by slug. */ | 'SLUG' /** Sort pages by title. */ @@ -19058,7 +19289,7 @@ export type PageTypeUpdateInput = { addAttributes?: InputMaybe>; /** Name of the page type. */ name?: InputMaybe; - /** List of attribute IDs to be unassigned from the page type. */ + /** List of attribute IDs to be assigned to the page type. */ removeAttributes?: InputMaybe>; /** Page type slug. */ slug?: InputMaybe; @@ -19133,21 +19364,6 @@ export type PasswordChange = { user: Maybe; }; -/** - * Controls whether password-based authentication is allowed. - * - * ENABLED - any user can log in with a password. This is the default behavior. - * CUSTOMERS_ONLY - only customer users can log in with a password. - * If a staff user logs in with a password, they will be treated as a customer - * — the issued token will not contain any staff permissions. - * DISABLED - no user can log in with a password. - * - */ -export type PasswordLoginModeEnum = - | 'CUSTOMERS_ONLY' - | 'DISABLED' - | 'ENABLED'; - /** Represents a payment of a given type. */ export type Payment = Node & ObjectWithMetadata & { /** @@ -19204,6 +19420,11 @@ export type Payment = Node & ObjectWithMetadata & { modified: Scalars['DateTime']['output']; /** Order associated with a payment. */ order: Maybe; + /** + * Informs whether this is a partial payment. + * @deprecated This field is reserved for the Adyen Gateway plugin. For other gateways, its value is always `false`. This field will be removed in 3.23 along with the plugin. + */ + partial: Scalars['Boolean']['output']; /** Type of method used for payment. */ paymentMethodType: Scalars['String']['output']; /** List of private metadata items. Requires staff permissions to access. */ @@ -19611,19 +19832,13 @@ export type PaymentMethodDetailsFilterInput = { }; /** - * Details of the payment method used for the transaction. One of `card`, `other`, or `giftCard` is required. + * Details of the payment method used for the transaction. One of `card` or `other` is required. * * Added in Saleor 3.22. */ export type PaymentMethodDetailsInput = { /** Details of the card payment method used for the transaction. */ card?: InputMaybe; - /** - * Details of the gift card payment method used for the transaction. - * - * Added in Saleor 3.23. - */ - giftCard?: InputMaybe; /** Details of the non-card payment method used for this transaction. */ other?: InputMaybe; }; @@ -19769,13 +19984,11 @@ export type PaymentMethodTokenizationResult = * The following types are possible: * CARD - represents a card payment method. * OTHER - represents any payment method that is not a card payment. - * GIFT_CARD - represents a gift card payment method. * * */ export type PaymentMethodTypeEnum = | 'CARD' - | 'GIFT_CARD' | 'OTHER'; export type PaymentMethodTypeEnumFilterInput = { @@ -20624,6 +20837,7 @@ export type ProductBulkCreateErrorCode = | 'ATTRIBUTE_VARIANTS_DISABLED' | 'BLANK' | 'DUPLICATED_INPUT_ITEM' + | 'FILE_SIZE_LIMIT_EXCEEDED' | 'GRAPHQL_ERROR' | 'INVALID' | 'INVALID_PRICE' @@ -21045,6 +21259,7 @@ export type ProductErrorCode = | 'ATTRIBUTE_VARIANTS_DISABLED' | 'CANNOT_MANAGE_PRODUCT_WITHOUT_VARIANT' | 'DUPLICATED_INPUT_ITEM' + | 'FILE_SIZE_LIMIT_EXCEEDED' | 'GRAPHQL_ERROR' | 'INVALID' | 'INVALID_FILE_TYPE' @@ -21059,7 +21274,8 @@ export type ProductErrorCode = | 'REQUIRED' | 'UNIQUE' | 'UNSUPPORTED_MEDIA_PROVIDER' - | 'UNSUPPORTED_MIME_TYPE'; + | 'UNSUPPORTED_MIME_TYPE' + | 'VARIANT_NO_DIGITAL_CONTENT'; /** Event sent when product export is completed. */ export type ProductExportCompleted = Event & { @@ -21654,17 +21870,11 @@ export type ProductType = Node & ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_PRODUCTS. */ availableAttributes: Maybe; - /** - * Whether the product type has variants. - * @deprecated This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. - */ + /** Whether the product type has variants. */ hasVariants: Scalars['Boolean']['output']; /** The ID of the product type. */ id: Scalars['ID']['output']; - /** - * Whether the product type is digital - doesn't have any effect, it's present for backward-compatibility. - * @deprecated Will be removed in v3.24.0, use metadata or attributes instead. - */ + /** Whether the product type is digital. */ isDigital: Scalars['Boolean']['output']; /** Whether shipping is required for this product type. */ isShippingRequired: Scalars['Boolean']['output']; @@ -21840,11 +22050,6 @@ export type ProductTypeEnum = | 'SHIPPABLE'; export type ProductTypeFilterInput = { - /** - * - * - * DEPRECATED: this field will be removed. The field has no effect on the API behavior. This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. - */ configurable?: InputMaybe; ids?: InputMaybe>; kind?: InputMaybe; @@ -21855,13 +22060,9 @@ export type ProductTypeFilterInput = { }; export type ProductTypeInput = { - /** - * Determines if product of this type has multiple variants. This option mainly simplifies product management in the dashboard. There is always at least one variant created under the hood. - * - * DEPRECATED: this field will be removed. The field has no effect on the API behavior. This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. - */ + /** Determines if product of this type has multiple variants. This option mainly simplifies product management in the dashboard. There is always at least one variant created under the hood. */ hasVariants?: InputMaybe; - /** Determines if products are digital - doesn't have any effect, it's present for backward-compatibility. */ + /** Determines if products are digital. */ isDigital?: InputMaybe; /** Determines if shipping is required for products of this variant. */ isShippingRequired?: InputMaybe; @@ -21994,6 +22195,12 @@ export type ProductVariant = Node & ObjectWithAttributes & ObjectWithMetadata & channelListings: Maybe>; /** The date and time when the product variant was created. */ created: Scalars['DateTime']['output']; + /** + * Digital content for the product variant. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + */ + digitalContent: Maybe; /** External ID of this product. */ externalReference: Maybe; /** The ID of the product variant. */ @@ -22393,9 +22600,7 @@ export type ProductVariantChannelListing = Node & { /** The price of the variant. */ price: Maybe; /** - * Previous price of the variant in channel. Useful for providing promotion information required by customer protection laws such as EU Omnibus directive. - * - * Warning: This field is not updated automatically. Use Channel Listings mutation to update it manually. + * Prior price of the variant used for discount calculations. * * Added in Saleor 3.21. */ @@ -24020,6 +24225,20 @@ export type Query = { * Requires one of the following permissions: MANAGE_ORDERS, MANAGE_USERS. */ customers: Maybe; + /** + * Look up digital content by ID. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContent: Maybe; + /** + * List of digital content. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContents: Maybe; /** * List of draft orders. The query will not initiate any external requests, including filtering available shipping methods, or performing external tax calculations. * @@ -24347,7 +24566,7 @@ export type Query = { export type Query_EntitiesArgs = { - representations: Array; + representations?: InputMaybe>>; }; @@ -24495,6 +24714,19 @@ export type QueryCustomersArgs = { }; +export type QueryDigitalContentArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryDigitalContentsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + export type QueryDraftOrdersArgs = { after?: InputMaybe; before?: InputMaybe; @@ -24888,7 +25120,6 @@ export type QueryTransactionsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; - sortBy?: InputMaybe; where?: InputMaybe; }; @@ -25033,7 +25264,7 @@ export type RefundSettingsErrorCode = export type RefundSettingsUpdate = { errors: Array; /** Refund settings. */ - refundSettings: Maybe; + refundSettings: RefundSettings; /** @deprecated Use `errors` field instead. */ refundSettingsErrors: Array; }; @@ -25784,10 +26015,7 @@ export type ShippingMethod = Node & ObjectWithMetadata & { id: Scalars['ID']['output']; /** Maximum delivery days for this shipping method. */ maximumDeliveryDays: Maybe; - /** - * Maximum order price for this shipping method. - * @deprecated No longer supported - */ + /** Maximum order price for this shipping method. */ maximumOrderPrice: Maybe; /** * Maximum order weight for this shipping method. @@ -25808,10 +26036,7 @@ export type ShippingMethod = Node & ObjectWithMetadata & { metafields: Maybe; /** Minimum delivery days for this shipping method. */ minimumDeliveryDays: Maybe; - /** - * Minimal order price for this shipping method. - * @deprecated No longer supported - */ + /** Minimal order price for this shipping method. */ minimumOrderPrice: Maybe; /** * Minimum order weight for this shipping method. @@ -26582,6 +26807,12 @@ export type Shop = ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_SETTINGS. */ allowLoginWithoutConfirmation: Maybe; + /** + * Enable automatic fulfillment for all digital products. + * + * Requires one of the following permissions: MANAGE_SETTINGS. + */ + automaticFulfillmentDigitalProducts: Maybe; /** List of available external authentications. */ availableExternalAuthentications: Array; /** List of available payment gateways. */ @@ -26615,6 +26846,18 @@ export type Shop = ObjectWithMetadata & { customerSetPasswordUrl: Maybe; /** Shop's default country. */ defaultCountry: Maybe; + /** + * Default number of max downloads per digital content URL. + * + * Requires one of the following permissions: MANAGE_SETTINGS. + */ + defaultDigitalMaxDownloads: Maybe; + /** + * Default number of days which digital content URL will be valid. + * + * Requires one of the following permissions: MANAGE_SETTINGS. + */ + defaultDigitalUrlValidDays: Maybe; /** * Default shop's email sender's address. * @@ -26684,12 +26927,6 @@ export type Shop = ObjectWithMetadata & { metafields: Maybe; /** Shop's name. */ name: Scalars['String']['output']; - /** - * Controls whether password-based authentication is allowed. - * - * Added in Saleor 3.23. - */ - passwordLoginMode: PasswordLoginModeEnum; /** List of available permissions. */ permissions: Array; /** List of possible phone prefixes. */ @@ -26736,12 +26973,6 @@ export type Shop = ObjectWithMetadata & { trackInventoryByDefault: Maybe; /** Returns translated shop fields for the given language code. */ translation: Maybe; - /** - * When enabled, stock availability is filtered by shipping zones and the destination address (legacy behavior). When disabled, stock availability is determined only by the direct warehouse-channel link, ignoring shipping zones. - * - * Added in Saleor 3.23. - */ - useLegacyShippingZoneStockAvailability: Scalars['Boolean']['output']; /** * Use legacy update webhook emission. When enabled, update webhooks (e.g. `customerUpdated`,`productVariantUpdated`) are sent even when only metadata changes. When disabled, update webhooks are not sent for metadata-only changes; only metadata-specific webhooks (e.g., `customerMetadataUpdated`, `productVariantMetadataUpdated`) are sent. * @@ -26849,7 +27080,6 @@ export type ShopErrorCode = | 'GRAPHQL_ERROR' | 'INVALID' | 'NOT_FOUND' - | 'PASSWORD_AUTH_RESTRICTION' | 'REQUIRED' | 'UNIQUE'; @@ -26883,6 +27113,8 @@ export type ShopMetadataUpdated = Event & { export type ShopSettingsInput = { /** Enable possibility to login without account confirmation. */ allowLoginWithoutConfirmation?: InputMaybe; + /** Enable automatic fulfillment for all digital products. */ + automaticFulfillmentDigitalProducts?: InputMaybe; /** * Charge taxes on shipping. * @@ -26891,6 +27123,10 @@ export type ShopSettingsInput = { chargeTaxesOnShipping?: InputMaybe; /** URL of a view where customers can set their password. */ customerSetPasswordUrl?: InputMaybe; + /** Default number of max downloads per digital content URL. */ + defaultDigitalMaxDownloads?: InputMaybe; + /** Default number of days which digital content URL will be valid. */ + defaultDigitalUrlValidDays?: InputMaybe; /** Default email sender's address. */ defaultMailSenderAddress?: InputMaybe; /** Default email sender's name. */ @@ -26927,12 +27163,6 @@ export type ShopSettingsInput = { * Warning: never store sensitive information, including financial data such as credit card details. */ metadata?: InputMaybe>; - /** - * Controls whether password-based authentication is allowed. - * - * Added in Saleor 3.23. - */ - passwordLoginMode?: InputMaybe; /** * When enabled, address fields that are not valid for a given country (according to Google's i18n address data) will be preserved instead of being removed during validation. Validation errors are still returned. * @@ -26951,12 +27181,6 @@ export type ShopSettingsInput = { reserveStockDurationAuthenticatedUser?: InputMaybe; /** This field is used as a default value for `ProductVariant.trackInventory`. */ trackInventoryByDefault?: InputMaybe; - /** - * When enabled, stock availability is filtered by shipping zones and the destination address (legacy behavior). When disabled, stock availability is determined only by the direct warehouse-channel link, ignoring shipping zones. - * - * Added in Saleor 3.23. - */ - useLegacyShippingZoneStockAvailability?: InputMaybe; /** * Use legacy update webhook emission. When enabled, update webhooks (e.g. `customerUpdated`,`productVariantUpdated`) are sent even when only metadata changes. When disabled, update webhooks are not sent for metadata-only changes; only metadata-specific webhooks (e.g., `customerMetadataUpdated`, `productVariantMetadataUpdated`) are sent. * @@ -28639,26 +28863,6 @@ export type TransactionEvent = Node & { type: Maybe; }; -/** - * Filter input for transaction events data. - * - * Added in Saleor 3.23. - */ -export type TransactionEventFilterInput = { - /** - * Filter transaction events by created at date. - * - * Added in Saleor 3.23. - */ - createdAt?: InputMaybe; - /** - * Filter transaction events by type. - * - * Added in Saleor 3.23. - */ - type?: InputMaybe; -}; - export type TransactionEventInput = { /** The message related to the event. */ message?: InputMaybe; @@ -28751,13 +28955,6 @@ export type TransactionEventTypeEnum = | 'REFUND_REVERSE' | 'REFUND_SUCCESS'; -export type TransactionEventTypeEnumFilterInput = { - /** The value equal to. */ - eq?: InputMaybe; - /** The value included in. */ - oneOf?: InputMaybe>; -}; - /** Filter input for transactions. */ export type TransactionFilterInput = { /** Filter by metadata fields of transactions. */ @@ -29107,27 +29304,6 @@ export type TransactionRequestRefundForGrantedRefundErrorCode = | 'REFUND_ALREADY_PROCESSED' | 'REFUND_IS_PENDING'; -export type TransactionSortField = - /** - * Sort transactions by creation date. - * - * Added in Saleor 3.23. - */ - | 'CREATED_AT' - /** - * Sort transactions by modification date. - * - * Added in Saleor 3.23. - */ - | 'MODIFIED_AT'; - -export type TransactionSortingInput = { - /** Specifies the direction in which to sort transactions. */ - direction: OrderDirection; - /** Sort transactions by the selected field. */ - field: TransactionSortField; -}; - /** * Update transaction. * @@ -29201,25 +29377,7 @@ export type TransactionWhereInput = { OR?: InputMaybe>; /** Filter by app identifier. */ appIdentifier?: InputMaybe; - /** - * Filter transactions by created at date. - * - * Added in Saleor 3.23. - */ - createdAt?: InputMaybe; - /** - * Filter by transaction events. Each list item represents conditions that must be satisfied by a single event. The filter matches transactions that have related events meeting all specified groups of conditions. - * - * Added in Saleor 3.23. - */ - events?: InputMaybe>; ids?: InputMaybe>; - /** - * Filter transactions by modified at date. - * - * Added in Saleor 3.23. - */ - modifiedAt?: InputMaybe; /** Filter by PSP reference. */ pspReference?: InputMaybe; }; @@ -29693,9 +29851,7 @@ export type UserSortField = /** Sort users by last name. */ | 'LAST_NAME' /** Sort users by order count. */ - | 'ORDER_COUNT' - /** Sort users by rank. Note: This option is available only with the `search` filter. */ - | 'RANK'; + | 'ORDER_COUNT'; export type UserSortingInput = { /** Specifies the direction in which to sort users. */ @@ -31910,6 +32066,15 @@ export type WeightUnitsEnum = | 'OZ' | 'TONNE'; +/** Represents the WIDGET target options for an app extension. */ +export type WidgetTargetOptions = { + /** + * HTTP method for Widget target (GET or POST) + * @deprecated Use `settings` field directly. + */ + method: HttpMethod; +}; + /** _Entity union as defined by Federation spec. */ export type _Entity = Address | App | Category | Collection | Group | Order | PageType | Product | ProductMedia | ProductType | ProductVariant | User; @@ -33122,7 +33287,7 @@ export type ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttr export type ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute_choices_AttributeValueCountableConnection = { edges: Array }; -export type ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute = { id: string, name: string, slug: string, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute_choices_AttributeValueCountableConnection | null }; +export type ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute = { id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductDetail_product_Product_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute_choices_AttributeValueCountableConnection | null }; export type ProductDetail_product_Product_assignedAttributes_AssignedFileAttribute_fileValue_File = { url: string, contentType: string | null }; @@ -33265,7 +33430,7 @@ export type ProductTypeDetail_productType_ProductType_productAttributes_Attribut export type ProductTypeDetail_productType_ProductType_productAttributes_Attribute_choices_AttributeValueCountableConnection = { edges: Array }; -export type ProductTypeDetail_productType_ProductType_productAttributes_Attribute = { id: string, name: string, slug: string, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductTypeDetail_productType_ProductType_productAttributes_Attribute_choices_AttributeValueCountableConnection | null }; +export type ProductTypeDetail_productType_ProductType_productAttributes_Attribute = { id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductTypeDetail_productType_ProductType_productAttributes_Attribute_choices_AttributeValueCountableConnection | null }; export type ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute_choices_AttributeValueCountableConnection_edges_AttributeValueCountableEdge_node_AttributeValue = { id: string, name: string | null, slug: string | null }; @@ -33273,7 +33438,7 @@ export type ProductTypeDetail_productType_ProductType_assignedVariantAttributes_ export type ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute_choices_AttributeValueCountableConnection = { edges: Array }; -export type ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute = { id: string, name: string, slug: string, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute_choices_AttributeValueCountableConnection | null }; +export type ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute = { id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute_choices_AttributeValueCountableConnection | null }; export type ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute = { variantSelection: boolean, attribute: ProductTypeDetail_productType_ProductType_assignedVariantAttributes_AssignedVariantAttribute_attribute_Attribute }; @@ -33295,7 +33460,7 @@ export type ProductTypesList_productTypes_ProductTypeCountableConnection_edges_P export type ProductTypesList_productTypes_ProductTypeCountableConnection_edges_ProductTypeCountableEdge_node_ProductType_productAttributes_Attribute_choices_AttributeValueCountableConnection = { edges: Array }; -export type ProductTypesList_productTypes_ProductTypeCountableConnection_edges_ProductTypeCountableEdge_node_ProductType_productAttributes_Attribute = { id: string, name: string, slug: string, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductTypesList_productTypes_ProductTypeCountableConnection_edges_ProductTypeCountableEdge_node_ProductType_productAttributes_Attribute_choices_AttributeValueCountableConnection | null }; +export type ProductTypesList_productTypes_ProductTypeCountableConnection_edges_ProductTypeCountableEdge_node_ProductType_productAttributes_Attribute = { id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductTypesList_productTypes_ProductTypeCountableConnection_edges_ProductTypeCountableEdge_node_ProductType_productAttributes_Attribute_choices_AttributeValueCountableConnection | null }; export type ProductTypesList_productTypes_ProductTypeCountableConnection_edges_ProductTypeCountableEdge_node_ProductType = { id: string, name: string, slug: string, hasVariants: boolean, productAttributes: Array | null }; @@ -33336,7 +33501,7 @@ export type ProductVariantDetail_productVariant_ProductVariant_assignedAttribute export type ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute_choices_AttributeValueCountableConnection = { edges: Array }; -export type ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute = { id: string, name: string, slug: string, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute_choices_AttributeValueCountableConnection | null }; +export type ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute = { id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, choices: ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedBooleanAttribute_attribute_Attribute_choices_AttributeValueCountableConnection | null }; export type ProductVariantDetail_productVariant_ProductVariant_assignedAttributes_AssignedFileAttribute_fileValue_File = { url: string, contentType: string | null }; @@ -33534,7 +33699,7 @@ export type VendorCustomerIdsVariables = Exact<{ export type VendorCustomerIds = VendorCustomerIds_Query; -export type VendorPageStatus_page_Page_attributes_SelectedAttribute_attribute_Attribute = { id: string, slug: string }; +export type VendorPageStatus_page_Page_attributes_SelectedAttribute_attribute_Attribute = { id: string, slug: string | null }; export type VendorPageStatus_page_Page_attributes_SelectedAttribute_values_AttributeValue_file_File = { url: string }; @@ -33554,7 +33719,7 @@ export type VendorPageStatusVariables = Exact<{ export type VendorPageStatus = VendorPageStatus_Query; -export type VendorPageType_pageTypes_PageTypeCountableConnection_edges_PageTypeCountableEdge_node_PageType_attributes_Attribute = { id: string, slug: string, inputType: AttributeInputTypeEnum | null }; +export type VendorPageType_pageTypes_PageTypeCountableConnection_edges_PageTypeCountableEdge_node_PageType_attributes_Attribute = { id: string, slug: string | null, inputType: AttributeInputTypeEnum | null }; export type VendorPageType_pageTypes_PageTypeCountableConnection_edges_PageTypeCountableEdge_node_PageType = { id: string, name: string, slug: string, attributes: Array | null }; diff --git a/apps/storefront/global.d.ts b/apps/storefront/global.d.ts index 49be4feb..bf920741 100644 --- a/apps/storefront/global.d.ts +++ b/apps/storefront/global.d.ts @@ -75,7 +75,8 @@ type RevalidateTag = | `CMS:${Slug}` | `COLLECTION:${Slug}` | `PRODUCT:${Slug}` - | `SEARCH:${Slug}`; + | `SEARCH:${Slug}` + | `MARKETPLACE:VENDOR:${Id}`; declare global { type RevalidateTag = RevalidateTag; diff --git a/apps/storefront/src/app/[locale]/(checkout)/checkout/page.tsx b/apps/storefront/src/app/[locale]/(checkout)/checkout/page.tsx index a1bdec4e..2c9edce4 100644 --- a/apps/storefront/src/app/[locale]/(checkout)/checkout/page.tsx +++ b/apps/storefront/src/app/[locale]/(checkout)/checkout/page.tsx @@ -5,11 +5,13 @@ import { type AllCountryCode } from "@nimara/domain/consts"; import { type AppErrorCode } from "@nimara/domain/objects/Error"; import { redirect } from "@nimara/i18n/routing"; +import { clientEnvs } from "@/envs/client"; import { getCheckoutOrRedirect, getMarketplaceCheckoutsOrRedirect, getMarketplaceCheckoutSummary, } from "@/features/checkout/checkout-actions"; +import { MARKETPLACE_NO_VENDOR_BUCKET } from "@/features/checkout/constants"; import { Summary } from "@/features/checkout/summary"; import { CHECKOUT_STEPS_MAP, @@ -23,7 +25,6 @@ import { getCheckoutPaymentSectionData } from "@/foundation/checkout/sections/pa import { paths } from "@/foundation/routing/paths"; import { getServiceRegistry } from "@/services/registry"; import { getAccessToken } from "@/services/tokens"; - interface PageProps { params: Promise<{ locale: Locale }>; searchParams: Promise<{ @@ -38,43 +39,43 @@ export const metadata: Metadata = { }; export default async function Page(props: PageProps) { - const isMarketplaceEnabled = - process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; - - if (isMarketplaceEnabled) { - return renderMarketplaceCheckoutPage(props); - } - - return renderLegacyCheckoutPage(props); -} + const isMarketplaceEnabled = clientEnvs.NEXT_PUBLIC_MARKETPLACE_ENABLED; -const renderLegacyCheckoutPage = async (props: PageProps) => { - const [{ locale }, searchParams, checkout, accessToken, services] = + const [{ locale }, searchParams, checkoutData, accessToken, services] = await Promise.all([ props.params, props.searchParams, - getCheckoutOrRedirect(), + isMarketplaceEnabled + ? getMarketplaceCheckoutsOrRedirect() + : getCheckoutOrRedirect(), getAccessToken(), getServiceRegistry(), ]); + const marketplaceCheckouts = Array.isArray(checkoutData) + ? checkoutData + : null; + const checkout = Array.isArray(checkoutData) + ? getMarketplaceCheckoutSummary(checkoutData) + : checkoutData; + const primaryCheckout = + marketplaceCheckouts?.find((item) => item.checkout.isShippingRequired) + ?.checkout ?? + marketplaceCheckouts?.[0].checkout ?? + checkout; + const currentStep = searchParams.step; if (!currentStep) { - let step: CheckoutStep | null = null; + let step: CheckoutStep; - const requiresEmail = checkout.email === null; - const requiresShipping = checkout.isShippingRequired; - const requiresShippingAddress = checkout.shippingAddress === null; - const requiresDeliveryMethod = checkout.deliveryMethod === null; - - if (requiresEmail) { + if (checkout.email === null) { step = CHECKOUT_STEPS_MAP.USER_DETAILS; - } else if (!requiresShipping) { + } else if (!checkout.isShippingRequired) { step = CHECKOUT_STEPS_MAP.PAYMENT; - } else if (requiresShippingAddress) { + } else if (checkout.shippingAddress === null) { step = CHECKOUT_STEPS_MAP.SHIPPING_ADDRESS; - } else if (requiresDeliveryMethod) { + } else if (checkout.deliveryMethod === null) { step = CHECKOUT_STEPS_MAP.DELIVERY_METHOD; } else { step = CHECKOUT_STEPS_MAP.PAYMENT; @@ -88,12 +89,12 @@ const renderLegacyCheckoutPage = async (props: PageProps) => { const userService = await services.getUserService(); const resultUserGet = await userService.userGet(accessToken); - const user = resultUserGet.ok ? resultUserGet.data : null; + const shippingAddressSectionData = checkout.isShippingRequired ? await getCheckoutShippingAddressSectionData({ accessToken, - checkout, + checkout: primaryCheckout, country: searchParams.country, locale, user, @@ -103,7 +104,7 @@ const renderLegacyCheckoutPage = async (props: PageProps) => { currentStep === CHECKOUT_STEPS_MAP.PAYMENT ? await getCheckoutPaymentSectionData({ accessToken, - checkout, + checkout: primaryCheckout, country: searchParams.country, errorCode: searchParams.errorCode, locale, @@ -111,118 +112,49 @@ const renderLegacyCheckoutPage = async (props: PageProps) => { }) : null; + const vendorIdNames: Record = {}; + + if (isMarketplaceEnabled) { + const marketplaceService = await services.getMarketplaceService(); + const vendorIds = [ + ...new Set(checkout.lines.map((line) => line.product.vendorId)), + ].filter(Boolean); + + await Promise.all( + vendorIds.map(async (vendorId) => { + if (vendorId === MARKETPLACE_NO_VENDOR_BUCKET) { + vendorIdNames[vendorId] = "Marketplace"; + } else { + const result = await marketplaceService.vendorGetByID(vendorId); + + if (result.ok) { + vendorIdNames[vendorId] = result.data.name; + } + } + }), + ); + } + return ( } - > - - - ); -}; - -const renderMarketplaceCheckoutPage = async (props: PageProps) => { - const [{ locale }, searchParams, checkoutItems, accessToken, services] = - await Promise.all([ - props.params, - props.searchParams, - getMarketplaceCheckoutsOrRedirect(), - getAccessToken(), - getServiceRegistry(), - ]); - - const checkoutSummary = getMarketplaceCheckoutSummary(checkoutItems); - const primaryCheckout = - checkoutItems.find((item) => item.checkout.isShippingRequired)?.checkout ?? - checkoutItems[0].checkout; - - const currentStep = searchParams.step; - - if (!currentStep) { - let step: CheckoutStep | null = null; - - const requiresEmail = checkoutItems.some( - (item) => item.checkout.email === null, - ); - const requiresShipping = checkoutItems.some( - (item) => item.checkout.isShippingRequired, - ); - const requiresShippingAddress = checkoutItems.some( - (item) => - item.checkout.isShippingRequired && - item.checkout.shippingAddress === null, - ); - const requiresDeliveryMethod = checkoutItems.some( - (item) => - item.checkout.isShippingRequired && - item.checkout.deliveryMethod === null, - ); - - if (requiresEmail) { - step = CHECKOUT_STEPS_MAP.USER_DETAILS; - } else if (!requiresShipping) { - step = CHECKOUT_STEPS_MAP.PAYMENT; - } else if (requiresShippingAddress) { - step = CHECKOUT_STEPS_MAP.SHIPPING_ADDRESS; - } else if (requiresDeliveryMethod) { - step = CHECKOUT_STEPS_MAP.DELIVERY_METHOD; - } else { - step = CHECKOUT_STEPS_MAP.PAYMENT; - } - - redirect({ - href: paths.checkout.asPath({ query: { step } }), - locale, - }); - } - - const userService = await services.getUserService(); - const resultUserGet = await userService.userGet(accessToken); - const user = resultUserGet.ok ? resultUserGet.data : null; - const shippingAddressSectionData = checkoutSummary.isShippingRequired - ? await getCheckoutShippingAddressSectionData({ - accessToken, - checkout: primaryCheckout, - country: searchParams.country, - locale, - user, - }) - : null; - const paymentSectionData = - currentStep === CHECKOUT_STEPS_MAP.PAYMENT - ? await getCheckoutPaymentSectionData({ - accessToken, - checkout: primaryCheckout, - country: searchParams.country, - errorCode: searchParams.errorCode, - locale, - user, - }) - : null; - - return ( - } > ); -}; +} diff --git a/apps/storefront/src/app/[locale]/(main)/cart/_actions/cart-actions.ts b/apps/storefront/src/app/[locale]/(main)/cart/_actions/cart-actions.ts index c5e0ada7..66cc0992 100644 --- a/apps/storefront/src/app/[locale]/(main)/cart/_actions/cart-actions.ts +++ b/apps/storefront/src/app/[locale]/(main)/cart/_actions/cart-actions.ts @@ -38,15 +38,15 @@ export const updateLineQuantityAction = async ({ // Handle Next.js-specific side effects (revalidation) if (result.ok) { void revalidateCart(cartId); + } else { + storefrontLogger.error("Failed to update line quantity", { + cartId, + lineId, + quantity, + errors: result.errors, + }); } - storefrontLogger.error("Failed to update line quantity", { - cartId, - lineId, - quantity, - errors: result.errors, - }); - return result; }; @@ -69,13 +69,13 @@ export const deleteLineAction = async ({ // Handle Next.js-specific side effects (revalidation) if (result.ok) { void revalidateCart(cartId); + } else { + storefrontLogger.error("Failed to delete line", { + cartId, + lineId, + errors: result.errors, + }); } - storefrontLogger.error("Failed to delete line", { - cartId, - lineId, - errors: result.errors, - }); - return result; }; diff --git a/apps/storefront/src/app/[locale]/(main)/cart/_components/marketplace-cart-view.tsx b/apps/storefront/src/app/[locale]/(main)/cart/_components/marketplace-cart-view.tsx index fc2fe39e..5704d792 100644 --- a/apps/storefront/src/app/[locale]/(main)/cart/_components/marketplace-cart-view.tsx +++ b/apps/storefront/src/app/[locale]/(main)/cart/_components/marketplace-cart-view.tsx @@ -1,10 +1,13 @@ import { type Cart } from "@nimara/domain/objects/Cart"; import { CartDetails } from "@nimara/features/cart/shared/components/cart-details"; +import { CartShell } from "@nimara/features/cart/shared/components/cart-shell"; import { EmptyCart } from "@nimara/features/cart/shared/components/empty-cart"; import { type CartViewProps } from "@nimara/features/cart/shared/types"; import { aggregateCarts } from "@/features/checkout/aggregations"; import { getCheckoutIdsByVendor } from "@/features/checkout/cart"; +import { MARKETPLACE_NO_VENDOR_BUCKET } from "@/features/checkout/constants"; +import { paths } from "@/foundation/routing/paths"; export const MarketplaceCartView = async (props: CartViewProps) => { const { @@ -22,7 +25,11 @@ export const MarketplaceCartView = async (props: CartViewProps) => { const checkoutIds = [...new Set(Object.values(checkoutIdsByVendor))]; if (!checkoutIds.length) { - return ; + return ( + + + + ); } const cartService = await services.getCartService(); @@ -58,7 +65,11 @@ export const MarketplaceCartView = async (props: CartViewProps) => { if (!cartsWithIds.length) { logger.debug("No active marketplace checkouts. Rendering empty cart."); - return ; + return ( + + + + ); } const { aggregatedCart, lineCheckoutIdMap } = aggregateCarts(cartsWithIds); @@ -69,22 +80,37 @@ export const MarketplaceCartView = async (props: CartViewProps) => { : { ok: false as const, errors: [], data: null }; const user = resultUserGet.ok ? resultUserGet.data : null; + const marketplaceService = await services.getMarketplaceService(); + const vendorIdNames: Record = {}; + + for (const [vendorId, _] of Object.entries(checkoutIdsByVendor)) { + if (vendorId === MARKETPLACE_NO_VENDOR_BUCKET) { + vendorIdNames[vendorId] = "Marketplace"; + } else { + const result = await marketplaceService.vendorGetByID(vendorId); + + if (result.ok) { + vendorIdNames[vendorId] = result.data.name; + } + } + } + return ( -
-
- -
-
+ + + ); }; diff --git a/apps/storefront/src/app/[locale]/(main)/cart/page.tsx b/apps/storefront/src/app/[locale]/(main)/cart/page.tsx index c9acac61..135359dd 100644 --- a/apps/storefront/src/app/[locale]/(main)/cart/page.tsx +++ b/apps/storefront/src/app/[locale]/(main)/cart/page.tsx @@ -3,6 +3,7 @@ import { StandardCartView, } from "@nimara/features/cart/shop-basic-cart/standard"; +import { clientEnvs } from "@/envs/client"; import { getCheckoutId, revalidateCart } from "@/features/checkout/cart"; import { getCurrentRegion } from "@/foundation/regions"; import { paths } from "@/foundation/routing/paths"; @@ -19,8 +20,7 @@ import { MarketplaceCartView } from "./_components/marketplace-cart-view"; export const generateMetadata = generateStandardCartMetadata; export default async function Page(props: any) { - const isMarketplaceEnabled = - process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; + const isMarketplaceEnabled = clientEnvs.NEXT_PUBLIC_MARKETPLACE_ENABLED; const [services, region, accessToken, checkoutId] = await Promise.all([ getServiceRegistry(), getCurrentRegion(), @@ -28,43 +28,37 @@ export default async function Page(props: any) { getCheckoutId(), ]); + const sharedProps = { + ...props, + services, + accessToken, + onCartUpdate: revalidateCart, + region, + logger: storefrontLogger, + onLineQuantityChange: updateLineQuantityAction, + onLineDelete: deleteLineAction, + paths: { + home: paths.home.asPath(), + checkout: paths.checkout.asPath(), + checkoutSignIn: paths.checkout.signIn.asPath(), + }, + }; + if (isMarketplaceEnabled) { return ( ); } return ( ); } diff --git a/apps/storefront/src/app/[locale]/(main)/payment/confirmation/page.tsx b/apps/storefront/src/app/[locale]/(main)/payment/confirmation/page.tsx index 807df3cf..f752b83e 100644 --- a/apps/storefront/src/app/[locale]/(main)/payment/confirmation/page.tsx +++ b/apps/storefront/src/app/[locale]/(main)/payment/confirmation/page.tsx @@ -3,6 +3,7 @@ import { type Locale } from "next-intl"; import { type AppErrorCode } from "@nimara/domain/objects/Error"; import { redirect } from "@nimara/i18n/routing"; +import { clientEnvs } from "@/envs/client"; import { getCheckoutOrRedirect } from "@/features/checkout/checkout-actions"; import { paths, QUERY_PARAMS } from "@/foundation/routing/paths"; import { getServiceRegistry } from "@/services/registry"; @@ -15,8 +16,7 @@ type PageProps = { }; export default async function Page(props: PageProps) { - const isMarketplaceEnabled = - process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; + const isMarketplaceEnabled = clientEnvs.NEXT_PUBLIC_MARKETPLACE_ENABLED; if (isMarketplaceEnabled) { const [{ locale }, searchParams] = await Promise.all([ diff --git a/apps/storefront/src/app/[locale]/(main)/products/[slug]/_actions/add-to-bag.ts b/apps/storefront/src/app/[locale]/(main)/products/[slug]/_actions/add-to-bag.ts index 2bfbc4d4..24ecd176 100644 --- a/apps/storefront/src/app/[locale]/(main)/products/[slug]/_actions/add-to-bag.ts +++ b/apps/storefront/src/app/[locale]/(main)/products/[slug]/_actions/add-to-bag.ts @@ -4,6 +4,7 @@ import { revalidatePath } from "next/cache"; import { addToBag } from "@nimara/features/product-detail-page/shared/actions/add-to-bag.core"; +import { clientEnvs } from "@/envs/client"; import { getCheckoutId, getCheckoutIdForVendor, @@ -17,8 +18,7 @@ import { storefrontLogger } from "@/services/logging"; import { getServiceRegistry } from "@/services/registry"; import { getAccessToken } from "@/services/tokens"; -const marketplaceEnabled = - process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; +const marketplaceEnabled = clientEnvs.NEXT_PUBLIC_MARKETPLACE_ENABLED; /** * Server action wrapper for adding items to the cart. diff --git a/apps/storefront/src/config.ts b/apps/storefront/src/config.ts index d8ff0e0f..cef3dbb3 100644 --- a/apps/storefront/src/config.ts +++ b/apps/storefront/src/config.ts @@ -28,4 +28,5 @@ export const MIN_PASSWORD_LENGTH = 8; export const CHANGE_EMAIL_TOKEN_VALIDITY_IN_HOURS = 72; -// TODO: group this somehow to make more logical +// Marketplace config +export const MARKETPLACE_VENDOR_PROFILE_CACHE_TTL = 7 * DAY; diff --git a/apps/storefront/src/features/checkout/cart.ts b/apps/storefront/src/features/checkout/cart.ts index 5d112127..741ae5f6 100644 --- a/apps/storefront/src/features/checkout/cart.ts +++ b/apps/storefront/src/features/checkout/cart.ts @@ -4,6 +4,7 @@ import { revalidatePath } from "next/cache"; import { cookies } from "next/headers"; import { COOKIE_KEY, COOKIE_MAX_AGE } from "@/config"; +import { clientEnvs } from "@/envs/client"; import { MARKETPLACE_NO_VENDOR_BUCKET } from "@/features/checkout/constants"; import { revalidateTag } from "@/foundation/cache/cache"; import { paths } from "@/foundation/routing/paths"; @@ -16,8 +17,7 @@ type MarketplaceCheckoutCookieV1 = { v: number; }; -const isMarketplaceEnabled = - process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; +const isMarketplaceEnabled = clientEnvs.NEXT_PUBLIC_MARKETPLACE_ENABLED; const sanitizeCheckoutId = (value: unknown): string | null => { if (typeof value !== "string") { diff --git a/apps/storefront/src/features/checkout/checkout-actions.ts b/apps/storefront/src/features/checkout/checkout-actions.ts index 6a862635..e1d06fa7 100644 --- a/apps/storefront/src/features/checkout/checkout-actions.ts +++ b/apps/storefront/src/features/checkout/checkout-actions.ts @@ -16,6 +16,7 @@ import { MARKETPLACE_NO_VENDOR_BUCKET } from "@/features/checkout/constants"; import { type MarketplaceCheckoutItem } from "@/features/checkout/types"; import { getCurrentRegion } from "@/foundation/regions"; import { paths } from "@/foundation/routing/paths"; +import { getMarketplaceService } from "@/services/marketplace"; import { getServiceRegistry } from "@/services/registry"; export const getCheckoutOrRedirect = async (): Promise | never => { @@ -60,7 +61,7 @@ const getVendorDisplayName = ( } if (vendorKey === MARKETPLACE_NO_VENDOR_BUCKET) { - return "No vendor"; + return "Marketplace"; } return vendorKey; @@ -105,13 +106,25 @@ export const getMarketplaceCheckoutsOrRedirect = async (): checkoutIdToVendorKey.get(checkoutId) ?? MARKETPLACE_NO_VENDOR_BUCKET; const checkout = result.data.checkout; + let displayName = "Marketplace"; + + if (vendorKey !== MARKETPLACE_NO_VENDOR_BUCKET) { + const marketplaceService = await services.getMarketplaceService(); + const vendorProfileResult = + await marketplaceService.vendorGetByID(vendorKey); + + if (vendorProfileResult.ok) { + displayName = vendorProfileResult.data.name; + } + } + await validateCheckoutLinesAction({ checkout, locale }); return { checkout, checkoutId, vendorKey, - vendorDisplayName: getVendorDisplayName(checkout, vendorKey), + vendorDisplayName: displayName, } satisfies MarketplaceCheckoutItem; }), ); diff --git a/apps/storefront/src/features/checkout/summary.tsx b/apps/storefront/src/features/checkout/summary.tsx index 159c1cbf..a43414f9 100644 --- a/apps/storefront/src/features/checkout/summary.tsx +++ b/apps/storefront/src/features/checkout/summary.tsx @@ -11,10 +11,17 @@ interface SummaryProps { }) => Promise>; checkout: Checkout; hidePromoCode?: boolean; + /** + * The mode of the checkout. Determines which summary UI to display. + * - "standard": Single checkout with promo code support + * - "marketplace": Aggregated multi-vendor checkout + */ + mode?: "standard" | "marketplace"; removePromoCodeAction?: (params: { checkoutId: string; promoCode: string; }) => Promise>; + vendorIdNames?: Record; } export const Summary = ({ @@ -22,14 +29,18 @@ export const Summary = ({ addPromoCodeAction, removePromoCodeAction, hidePromoCode = false, + mode = "standard", + vendorIdNames, }: SummaryProps) => { return ( {!hidePromoCode && ( { const resultUserGet = await userService.userGet(accessToken); const user = resultUserGet.ok ? resultUserGet.data : null; - const isMarketplaceEnabled = - process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; + const isMarketplaceEnabled = clientEnvs.NEXT_PUBLIC_MARKETPLACE_ENABLED; let checkoutLinesCount = 0; const checkoutIds = isMarketplaceEnabled ? await getCheckoutIds() diff --git a/apps/storefront/src/foundation/auth/login.ts b/apps/storefront/src/foundation/auth/login.ts index fd9e2fff..26ffb26e 100644 --- a/apps/storefront/src/foundation/auth/login.ts +++ b/apps/storefront/src/foundation/auth/login.ts @@ -5,6 +5,7 @@ import { AuthError } from "next-auth"; import { signIn } from "@/auth"; import { CACHE_TTL } from "@/config"; +import { clientEnvs } from "@/envs/client"; import { getCheckoutId, getCheckoutIds, @@ -27,8 +28,7 @@ export async function login({ redirectUrl?: string; }) { try { - const isMarketplaceEnabled = - process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; + const isMarketplaceEnabled = clientEnvs.NEXT_PUBLIC_MARKETPLACE_ENABLED; await signIn("credentials", { email, diff --git a/apps/storefront/src/foundation/checkout/actions/update-checkout-address-action.ts b/apps/storefront/src/foundation/checkout/actions/update-checkout-address-action.ts index 74138899..13d0e257 100644 --- a/apps/storefront/src/foundation/checkout/actions/update-checkout-address-action.ts +++ b/apps/storefront/src/foundation/checkout/actions/update-checkout-address-action.ts @@ -9,6 +9,7 @@ import { import { type Checkout } from "@nimara/domain/objects/Checkout"; import { type AsyncResult, ok } from "@nimara/domain/objects/Result"; +import { clientEnvs } from "@/envs/client"; import { getCheckoutIds } from "@/features/checkout/cart"; import { paths } from "@/foundation/routing/paths"; import { getServiceRegistry } from "@/services/registry"; @@ -36,8 +37,7 @@ export const updateCheckoutAddressAction = async ({ }): AsyncResult<{ success: true; }> => { - const isMarketplaceEnabled = - process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; + const isMarketplaceEnabled = clientEnvs.NEXT_PUBLIC_MARKETPLACE_ENABLED; const services = await getServiceRegistry(); const checkoutService = await services.getCheckoutService(); diff --git a/apps/storefront/src/foundation/checkout/sections/checkout-section.tsx b/apps/storefront/src/foundation/checkout/sections/checkout-section.tsx index ed2a01b1..e5b1f8e5 100644 --- a/apps/storefront/src/foundation/checkout/sections/checkout-section.tsx +++ b/apps/storefront/src/foundation/checkout/sections/checkout-section.tsx @@ -12,7 +12,7 @@ import { type CheckoutStep } from "@/foundation/checkout/steps"; import { paths } from "@/foundation/routing/paths"; interface Props { - closedContent?: React.ReactNode; + collapsedSummary?: React.ReactNode; disabled?: boolean; isComplete: boolean; isOpen: boolean; @@ -26,7 +26,7 @@ export const CheckoutSection = ({ children, isOpen, isComplete, - closedContent, + collapsedSummary, disabled, }: PropsWithChildren) => { return ( @@ -65,7 +65,7 @@ export const CheckoutSection = ({ {children} ) : ( - closedContent && {closedContent} + collapsedSummary && {collapsedSummary} )} diff --git a/apps/storefront/src/foundation/checkout/sections/delivery-method/marketplace-form.tsx b/apps/storefront/src/foundation/checkout/sections/delivery-method/marketplace-form.tsx index b57c9063..325d3678 100644 --- a/apps/storefront/src/foundation/checkout/sections/delivery-method/marketplace-form.tsx +++ b/apps/storefront/src/foundation/checkout/sections/delivery-method/marketplace-form.tsx @@ -77,15 +77,20 @@ export const MarketplaceDeliveryMethodForm = ({ return (
{checkoutItems.map(({ checkout, checkoutId, vendorDisplayName }) => ( -
-

- Vendor: {vendorDisplayName} -

+
+
+ + Sells and delivered by + + + {vendorDisplayName} + +
(

{method.name}

-

+

{formatter.number(method.price.amount, { style: "currency", currency: method.price.currency, diff --git a/apps/storefront/src/foundation/checkout/sections/delivery-method/section.tsx b/apps/storefront/src/foundation/checkout/sections/delivery-method/section.tsx index 3338ff28..0f4e2a18 100644 --- a/apps/storefront/src/foundation/checkout/sections/delivery-method/section.tsx +++ b/apps/storefront/src/foundation/checkout/sections/delivery-method/section.tsx @@ -1,5 +1,5 @@ import { useTranslations } from "next-intl"; -import { type PropsWithChildren } from "react"; +import { type PropsWithChildren, type ReactNode } from "react"; import { type Checkout } from "@nimara/domain/objects/Checkout"; @@ -7,6 +7,7 @@ import { CheckoutSection } from "../checkout-section"; interface CheckoutDeliveryMethodSectionProps { checkout: Checkout; + collapsedSummary?: ReactNode; isOpen: boolean; } @@ -14,12 +15,19 @@ export const CheckoutDeliveryMethodSection = ({ checkout, isOpen, children, + collapsedSummary, }: PropsWithChildren) => { const t = useTranslations(); const isDeliveryMethodProvided = checkout.deliveryMethod !== null; const disabled = !isDeliveryMethodProvided; + const defaultSummary = checkout.deliveryMethod ? ( +

+ {checkout.deliveryMethod.name} +

+ ) : null; + return ( - {checkout.deliveryMethod.name} -

- ) : null - } + collapsedSummary={collapsedSummary ?? defaultSummary} > {children}
diff --git a/apps/storefront/src/foundation/checkout/sections/payment/payment.tsx b/apps/storefront/src/foundation/checkout/sections/payment/payment.tsx index e63e0984..7236e2a8 100644 --- a/apps/storefront/src/foundation/checkout/sections/payment/payment.tsx +++ b/apps/storefront/src/foundation/checkout/sections/payment/payment.tsx @@ -35,6 +35,7 @@ import { import { useToast } from "@nimara/ui/hooks"; import { cn } from "@nimara/ui/lib/utils"; +import { clientEnvs } from "@/envs/client"; import { PAYMENT_ELEMENT_ID } from "@/features/checkout/consts"; import { PaymentMethods } from "@/features/checkout/payment-methods"; import { type MarketplaceCheckoutItem } from "@/features/checkout/types"; @@ -124,7 +125,7 @@ export const Payment = ({ const [isMounted, setIsMounted] = useState(false); const [isCountryChanging, setIsCountryChanging] = useState(false); const isMarketplacePayment = - process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false" && + clientEnvs.NEXT_PUBLIC_MARKETPLACE_ENABLED && !!marketplaceCheckouts && marketplaceCheckouts.length > 0; const hasSavedPaymentMethods = diff --git a/apps/storefront/src/foundation/checkout/sections/sections.tsx b/apps/storefront/src/foundation/checkout/sections/sections.tsx index 77a55e83..fc59ed66 100644 --- a/apps/storefront/src/foundation/checkout/sections/sections.tsx +++ b/apps/storefront/src/foundation/checkout/sections/sections.tsx @@ -45,11 +45,11 @@ export const CheckoutSections = ({ }: Props) => { const t = useTranslations(); const router = useRouter(); - const isMarketplaceFlow = + const isMarketplaceMode = clientEnvs.NEXT_PUBLIC_MARKETPLACE_ENABLED && !!marketplaceCheckouts && marketplaceCheckouts.length > 0; - const checkoutCollection = isMarketplaceFlow + const checkoutCollection = isMarketplaceMode ? marketplaceCheckouts.map((item) => item.checkout) : [checkout]; const emailProvidedForAll = checkoutCollection.every( @@ -64,7 +64,7 @@ export const CheckoutSections = ({ const isShippingRequiredForAny = checkoutCollection.some( (entry) => entry.isShippingRequired, ); - const marketplaceShippingCheckouts = marketplaceCheckouts?.filter( + const checkoutsWithShippingRequired = marketplaceCheckouts?.filter( (item) => item.checkout.isShippingRequired, ); const checkoutForSections: Checkout = { @@ -89,6 +89,14 @@ export const CheckoutSections = ({ : null, }; + const submitDeliveryMethod = () => { + router.push( + paths.checkout.asPath({ + query: { step: "payment" }, + }), + ); + }; + return ( )} - {checkoutForSections.isShippingRequired && ( + {isMarketplaceMode && checkoutsWithShippingRequired && ( <> - {isMarketplaceFlow && marketplaceShippingCheckouts ? ( - { - router.push( - paths.checkout.asPath({ - query: { step: "payment" }, - }), - ); - }} - /> - ) : ( - { - router.push( - paths.checkout.asPath({ - query: { step: "payment" }, - }), - ); - }} - /> + collapsedSummary={checkoutsWithShippingRequired.map( + ({ checkout, vendorDisplayName }) => ( +
+ {checkout.deliveryMethod?.name} + - + {vendorDisplayName} +
+ ), )} + > + +
+ + )} + + {!isMarketplaceMode && checkoutForSections.isShippingRequired && ( + <> + + + {checkoutForSections.deliveryMethod.name} +

+ ) : null + } + > +
)} diff --git a/apps/storefront/src/foundation/checkout/sections/shipping-address/section.tsx b/apps/storefront/src/foundation/checkout/sections/shipping-address/section.tsx index f999afd6..4984340f 100644 --- a/apps/storefront/src/foundation/checkout/sections/shipping-address/section.tsx +++ b/apps/storefront/src/foundation/checkout/sections/shipping-address/section.tsx @@ -26,7 +26,7 @@ export const CheckoutShippingAddressSection = ({ step="shipping-address" title={t("shipping-address.title")} isOpen={isOpen} - closedContent={ + collapsedSummary={ checkout.shippingAddress && formattedShippingAddress ? (
{displayFormattedAddressLines({ diff --git a/apps/storefront/src/foundation/checkout/sections/user-details/actions.ts b/apps/storefront/src/foundation/checkout/sections/user-details/actions.ts index f5139e7c..7c3bca45 100644 --- a/apps/storefront/src/foundation/checkout/sections/user-details/actions.ts +++ b/apps/storefront/src/foundation/checkout/sections/user-details/actions.ts @@ -5,6 +5,7 @@ import { revalidatePath } from "next/cache"; import { type Checkout } from "@nimara/domain/objects/Checkout"; import { ok } from "@nimara/domain/objects/Result"; +import { clientEnvs } from "@/envs/client"; import { serverEnvs } from "@/envs/server"; import { getCheckoutIds } from "@/features/checkout/cart"; import { paths } from "@/foundation/routing/paths"; @@ -47,8 +48,7 @@ export const updateCheckoutUserDetailsAction = async ( payload: UpdateCheckoutUserDetailsPayload, opts: UpdateCheckoutUserDetailsOpts = {}, ) => { - const isMarketplaceEnabled = - process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== "false"; + const isMarketplaceEnabled = clientEnvs.NEXT_PUBLIC_MARKETPLACE_ENABLED; const services = await getServiceRegistry(); const checkoutService = await services.getCheckoutService(); diff --git a/apps/storefront/src/foundation/checkout/sections/user-details/section.tsx b/apps/storefront/src/foundation/checkout/sections/user-details/section.tsx index e86b1a4f..04155b33 100644 --- a/apps/storefront/src/foundation/checkout/sections/user-details/section.tsx +++ b/apps/storefront/src/foundation/checkout/sections/user-details/section.tsx @@ -23,7 +23,7 @@ export const CheckoutUserDetailsSection = ({ step="user-details" title={t("user-details.title")} isOpen={isOpen} - closedContent={ + collapsedSummary={ checkout.email ? (

{checkout.email}

) : null diff --git a/apps/storefront/src/services/lazy-loaders/marketplace.ts b/apps/storefront/src/services/lazy-loaders/marketplace.ts new file mode 100644 index 00000000..545afa0b --- /dev/null +++ b/apps/storefront/src/services/lazy-loaders/marketplace.ts @@ -0,0 +1,35 @@ +import { type Logger } from "@nimara/foundation/logging/types"; +import { type MarketplaceService } from "@nimara/infrastructure/marketplace/types"; + +import { MARKETPLACE_VENDOR_PROFILE_CACHE_TTL } from "@/config"; +import { clientEnvs } from "@/envs/client"; + +/** + * Creates a lazy loader function for the checkout service. + * This function is only used by the service registry. + * @internal + * @param logger - The logger to use for the checkout service. + * @returns A promise that resolves to the checkout service. + */ +export const createMarketplaceServiceLoader = (logger: Logger) => { + let marketplaceServiceInstance: MarketplaceService | null = null; + + return async (): Promise => { + if (marketplaceServiceInstance) { + return marketplaceServiceInstance; + } + + const { saleorMarketplaceService } = + await import("@nimara/infrastructure/marketplace/saleor/service"); + + marketplaceServiceInstance = saleorMarketplaceService({ + apiURL: clientEnvs.NEXT_PUBLIC_SALEOR_API_URL, + logger, + cacheTTL: { + vendorProfile: MARKETPLACE_VENDOR_PROFILE_CACHE_TTL, + }, + }); + + return marketplaceServiceInstance; + }; +}; diff --git a/apps/storefront/src/services/marketplace.ts b/apps/storefront/src/services/marketplace.ts new file mode 100644 index 00000000..6017720f --- /dev/null +++ b/apps/storefront/src/services/marketplace.ts @@ -0,0 +1,7 @@ +import { getServiceRegistry } from "@/services/registry"; + +export const getMarketplaceService = async () => { + const serviceRegistry = await getServiceRegistry(); + + return serviceRegistry.getMarketplaceService(); +}; diff --git a/apps/storefront/src/services/registry.ts b/apps/storefront/src/services/registry.ts index ef14edfe..7b58619c 100644 --- a/apps/storefront/src/services/registry.ts +++ b/apps/storefront/src/services/registry.ts @@ -4,6 +4,7 @@ import type { ServiceRegistry } from "@nimara/infrastructure/types"; import { CACHE_TTL } from "@/config"; import { createAddressServiceLoader } from "@/services/lazy-loaders/address"; import { createCheckoutServiceLoader } from "@/services/lazy-loaders/checkout"; +import { createMarketplaceServiceLoader } from "@/services/lazy-loaders/marketplace"; import { createPaymentServiceLoader } from "@/services/lazy-loaders/payment"; import { createCartServiceLoader } from "./lazy-loaders/cart"; @@ -44,6 +45,7 @@ export const getServiceRegistry = async (): Promise => { const getSearchService = createSearchServiceLoader(logger); const getStoreService = createStoreServiceLoader(logger); const getUserService = createUserServiceLoader(logger); + const getMarketplaceService = createMarketplaceServiceLoader(logger); serviceRegistryInstance = { config, @@ -57,6 +59,7 @@ export const getServiceRegistry = async (): Promise => { getStoreService, getUserService, getPaymentService, + getMarketplaceService, }; return serviceRegistryInstance; diff --git a/apps/stripe/src/graphql/generated/client.ts b/apps/stripe/src/graphql/generated/client.ts index 1eaf4023..df7a0ebc 100644 --- a/apps/stripe/src/graphql/generated/client.ts +++ b/apps/stripe/src/graphql/generated/client.ts @@ -268,8 +268,8 @@ export type AccountErrorCode = | "DELETE_OWN_ACCOUNT" | "DELETE_STAFF_ACCOUNT" | "DELETE_SUPERUSER_ACCOUNT" - | "DISABLED_AUTHENTICATION_METHOD" | "DUPLICATED_INPUT_ITEM" + | "FILE_SIZE_LIMIT_EXCEEDED" | "GRAPHQL_ERROR" | "INACTIVE" | "INVALID" @@ -1049,21 +1049,38 @@ export type AppExtension = Node & { /** Label of the extension to show in the dashboard. */ label: Scalars["String"]["output"]; /** - * Name of the extension mount point in the dashboard. Value returned in UPPERCASE. + * Place where given extension will be mounted. + * @deprecated Use `mountName` instead. + */ + mount: AppExtensionMountEnum; + /** + * Name of the extension mount point in the dashboard. Replaces `mount` * * Added in Saleor 3.22. */ mountName: Scalars["String"]["output"]; + /** + * App extension options. + * + * Added in Saleor 3.22. + * @deprecated Use `settings` field instead. + */ + options: Maybe; /** List of the app extension's permissions. */ permissions: Array; /** - * App extension settings. + * App extension settings. Replaces `options` field. * * Added in Saleor 3.22. */ settings: Scalars["JSON"]["output"]; /** - * Name of the extension target in the dashboard. Value returned in UPPERCASE. + * Type of way how app extension will be opened. + * @deprecated Use `targetName` instead. + */ + target: AppExtensionTargetEnum; + /** + * Name of the extension target in the dashboard. Replaces `target` * * Added in Saleor 3.22. */ @@ -1088,12 +1105,24 @@ export type AppExtensionCountableEdge = { }; export type AppExtensionFilterInput = { + /** + * DEPRECATED: Use `mountName` instead. + * + * DEPRECATED: this field will be removed. + */ + mount?: InputMaybe>; /** * Plain-text mount name (case insensitive) * * Added in Saleor 3.22. */ mountName?: InputMaybe>; + /** + * DEPRECATED: Use `targetName` instead. + * + * DEPRECATED: this field will be removed. + */ + target?: InputMaybe; /** * Plain-text target name (case insensitive) * @@ -1102,6 +1131,94 @@ export type AppExtensionFilterInput = { targetName?: InputMaybe; }; +/** All places where app extension can be mounted. */ +export type AppExtensionMountEnum = + | "CATEGORY_DETAILS_MORE_ACTIONS" + | "CATEGORY_OVERVIEW_CREATE" + | "CATEGORY_OVERVIEW_MORE_ACTIONS" + | "COLLECTION_DETAILS_MORE_ACTIONS" + | "COLLECTION_DETAILS_WIDGETS" + | "COLLECTION_OVERVIEW_CREATE" + | "COLLECTION_OVERVIEW_MORE_ACTIONS" + | "CUSTOMER_DETAILS_MORE_ACTIONS" + | "CUSTOMER_DETAILS_WIDGETS" + | "CUSTOMER_OVERVIEW_CREATE" + | "CUSTOMER_OVERVIEW_MORE_ACTIONS" + | "DISCOUNT_DETAILS_MORE_ACTIONS" + | "DISCOUNT_OVERVIEW_CREATE" + | "DISCOUNT_OVERVIEW_MORE_ACTIONS" + | "DRAFT_ORDER_DETAILS_MORE_ACTIONS" + | "DRAFT_ORDER_DETAILS_WIDGETS" + | "DRAFT_ORDER_OVERVIEW_CREATE" + | "DRAFT_ORDER_OVERVIEW_MORE_ACTIONS" + | "GIFT_CARD_DETAILS_MORE_ACTIONS" + | "GIFT_CARD_DETAILS_WIDGETS" + | "GIFT_CARD_OVERVIEW_CREATE" + | "GIFT_CARD_OVERVIEW_MORE_ACTIONS" + | "MENU_DETAILS_MORE_ACTIONS" + | "MENU_OVERVIEW_CREATE" + | "MENU_OVERVIEW_MORE_ACTIONS" + | "NAVIGATION_CATALOG" + | "NAVIGATION_CUSTOMERS" + | "NAVIGATION_DISCOUNTS" + | "NAVIGATION_ORDERS" + | "NAVIGATION_PAGES" + | "NAVIGATION_TRANSLATIONS" + | "ORDER_DETAILS_MORE_ACTIONS" + | "ORDER_DETAILS_WIDGETS" + | "ORDER_OVERVIEW_CREATE" + | "ORDER_OVERVIEW_MORE_ACTIONS" + | "PAGE_DETAILS_MORE_ACTIONS" + | "PAGE_OVERVIEW_CREATE" + | "PAGE_OVERVIEW_MORE_ACTIONS" + | "PAGE_TYPE_DETAILS_MORE_ACTIONS" + | "PAGE_TYPE_OVERVIEW_CREATE" + | "PAGE_TYPE_OVERVIEW_MORE_ACTIONS" + | "PRODUCT_DETAILS_MORE_ACTIONS" + | "PRODUCT_DETAILS_WIDGETS" + | "PRODUCT_OVERVIEW_CREATE" + | "PRODUCT_OVERVIEW_MORE_ACTIONS" + | "TRANSLATIONS_MORE_ACTIONS" + | "VOUCHER_DETAILS_MORE_ACTIONS" + | "VOUCHER_DETAILS_WIDGETS" + | "VOUCHER_OVERVIEW_CREATE" + | "VOUCHER_OVERVIEW_MORE_ACTIONS"; + +/** Represents the options for an app extension. */ +export type AppExtensionOptionsNewTab = { + /** + * Options controlling behavior of the NEW_TAB extension target + * @deprecated Use `settings` field directly. + */ + newTabTarget: Maybe; +}; + +/** Represents the options for an app extension. */ +export type AppExtensionOptionsWidget = { + /** + * Options for displaying a Widget + * @deprecated Use `settings` field directly. + */ + widgetTarget: Maybe; +}; + +export type AppExtensionPossibleOptions = + | AppExtensionOptionsNewTab + | AppExtensionOptionsWidget; + +/** + * All available ways of opening an app extension. + * + * POPUP - app's extension will be mounted as a popup window + * APP_PAGE - redirect to app's page + * + */ +export type AppExtensionTargetEnum = + | "APP_PAGE" + | "NEW_TAB" + | "POPUP" + | "WIDGET"; + /** * Fetch and validate manifest. * @@ -1146,9 +1263,9 @@ export type AppInstallInput = { /** Determine if app will be set active or not. */ activateAfterInstallation?: InputMaybe; /** Name of the app to install. */ - appName: Scalars["String"]["input"]; + appName?: InputMaybe; /** URL to app's manifest in JSON format. */ - manifestUrl: Scalars["String"]["input"]; + manifestUrl?: InputMaybe; /** List of permission code names to assign to this app. */ permissions?: InputMaybe>; }; @@ -1210,7 +1327,12 @@ export type AppManifestExtension = { /** Label of the extension to show in the dashboard. */ label: Scalars["String"]["output"]; /** - * Name of the extension mount point in the dashboard. Value returned in UPPERCASE. + * Place where given extension will be mounted. + * @deprecated Use `mountName` instead. + */ + mount: AppExtensionMountEnum; + /** + * Name of the extension mount point in the dashboard. Replaces `mount` * * Added in Saleor 3.22. */ @@ -1218,13 +1340,18 @@ export type AppManifestExtension = { /** List of the app extension's permissions. */ permissions: Array; /** - * App extension settings. + * JSON object with settings for this extension. * * Added in Saleor 3.22. */ settings: Scalars["JSON"]["output"]; /** - * Name of the extension target in the dashboard. Value returned in UPPERCASE. + * Type of way how app extension will be opened. + * @deprecated Use `targetName` instead. + */ + target: AppExtensionTargetEnum; + /** + * Name of the extension target in the dashboard. Replaces `target` * * Added in Saleor 3.22. */ @@ -2059,7 +2186,7 @@ export type Attribute = Node & /** Public metadata. Use `keys` to control which fields you want to include. The default is to include everything. */ metafields: Maybe; /** Name of an attribute displayed in the interface. */ - name: Scalars["String"]["output"]; + name: Maybe; /** List of private metadata items. Requires staff permissions to access. */ privateMetadata: Array; /** @@ -2081,7 +2208,7 @@ export type Attribute = Node & */ referenceTypes: Maybe>; /** Internal representation of an attribute name. */ - slug: Scalars["String"]["output"]; + slug: Maybe; /** * The position of the attribute in the storefront navigation (0 by default). Requires one of the following permissions: MANAGE_PAGES, MANAGE_PAGE_TYPES_AND_ATTRIBUTES, MANAGE_PRODUCTS, MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES. * @deprecated No longer supported @@ -2090,7 +2217,7 @@ export type Attribute = Node & /** Returns translated attribute fields for the given language code. */ translation: Maybe; /** The attribute type. */ - type: AttributeTypeEnum; + type: Maybe; /** The unit of attribute values. */ unit: Maybe; /** Whether the attribute requires values to be passed or not. Requires one of the following permissions: MANAGE_PAGES, MANAGE_PAGE_TYPES_AND_ATTRIBUTES, MANAGE_PRODUCTS, MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES. */ @@ -4199,19 +4326,12 @@ export type Checkout = Node & * Added in Saleor 3.21. */ customerNote: Scalars["String"]["output"]; - /** - * The delivery method selected for this checkout. - * - * Added in Saleor 3.23. - */ - delivery: Maybe; /** * The delivery method selected for this checkout. * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Optionally triggered when cached external shipping methods are invalid. * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. - * @deprecated Use `delivery` instead. */ deliveryMethod: Maybe; /** The total discount applied to the checkout. Note: Only discount created via voucher are included in this field. */ @@ -4271,7 +4391,7 @@ export type Checkout = Node & * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Optionally triggered when cached external shipping methods are invalid. * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. - * @deprecated Use `delivery` instead. + * @deprecated Use `deliveryMethod` instead. */ shippingMethod: Maybe; /** @@ -4669,7 +4789,6 @@ export type CheckoutCustomerNoteUpdate = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ export type CheckoutDeliveryMethodUpdate = { @@ -5069,27 +5188,7 @@ export type CheckoutPaymentCreate = { /** Represents an problem in the checkout. */ export type CheckoutProblem = | CheckoutLineProblemInsufficientStock - | CheckoutLineProblemVariantNotAvailable - | CheckoutProblemDeliveryMethodInvalid - | CheckoutProblemDeliveryMethodStale; - -/** - * Indicates that the selected delivery method is invalid. - * - * Added in Saleor 3.23. - */ -export type CheckoutProblemDeliveryMethodInvalid = { - delivery: Delivery; -}; - -/** - * Indicates that the delivery methods are stale. - * - * Added in Saleor 3.23. - */ -export type CheckoutProblemDeliveryMethodStale = { - delivery: Delivery; -}; + | CheckoutLineProblemVariantNotAvailable; /** * Remove a gift card or a voucher from a checkout. @@ -5107,12 +5206,6 @@ export type CheckoutRemovePromoCode = { /** Represents the channel-specific checkout settings. */ export type CheckoutSettings = { - /** - * Default to `true`. Determines whether gift cards can be attached to a Checkout via `addPromoCode` mutation. Usage of this mutation with gift cards is deprecated. - * - * Added in Saleor 3.23. - */ - allowLegacyGiftCardUse: Scalars["Boolean"]["output"]; /** * The date time defines the earliest checkout creation date on which fully paid checkouts can begin to be automatically completed. * @@ -5140,12 +5233,6 @@ export type CheckoutSettings = { }; export type CheckoutSettingsInput = { - /** - * Default to `true`. Determines whether gift cards can be attached to a Checkout via `addPromoCode` mutation. Usage of this mutation with gift cards is deprecated. - * - * Added in Saleor 3.23. - */ - allowLegacyGiftCardUse?: InputMaybe; /** * Settings for automatic completion of fully paid checkouts. * @@ -5189,7 +5276,6 @@ export type CheckoutShippingAddressUpdate = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout shipping method with the external one. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ export type CheckoutShippingMethodUpdate = { @@ -5206,9 +5292,7 @@ export type CheckoutSortField = /** Sort checkouts by customer. */ | "CUSTOMER" /** Sort checkouts by payment. */ - | "PAYMENT" - /** Sort checkouts by rank. Note: This option is available only with the `search` filter. */ - | "RANK"; + | "PAYMENT"; export type CheckoutSortingInput = { /** Specifies the direction in which to sort checkouts. */ @@ -5567,6 +5651,7 @@ export type CollectionError = { export type CollectionErrorCode = | "CANNOT_MANAGE_PRODUCT_WITHOUT_VARIANT" | "DUPLICATED_INPUT_ITEM" + | "FILE_SIZE_LIMIT_EXCEEDED" | "GRAPHQL_ERROR" | "INVALID" | "NOT_FOUND" @@ -6619,6 +6704,8 @@ export type CustomerEvent = Node & { message: Maybe; /** The concerned order. */ order: Maybe; + /** The concerned order line. */ + orderLine: Maybe; /** Customer event type. */ type: Maybe; /** User who performed the action. */ @@ -6884,49 +6971,204 @@ export type DeletePrivateMetadata = { metadataErrors: Array; }; +/** Represents a delivery method chosen for the checkout. `Warehouse` type is used when checkout is marked as "click and collect" and `ShippingMethod` otherwise. */ +export type DeliveryMethod = ShippingMethod | Warehouse; + +/** Represents digital content associated with a product variant. */ +export type DigitalContent = Node & + ObjectWithMetadata & { + /** Indicator for automatic fulfillment of digital content. */ + automaticFulfillment: Scalars["Boolean"]["output"]; + /** File associated with digital content. */ + contentFile: Scalars["String"]["output"]; + /** The ID of the digital content. */ + id: Scalars["ID"]["output"]; + /** Maximum number of allowed downloads for the digital content. */ + maxDownloads: Maybe; + /** List of public metadata items. Can be accessed without permissions. */ + metadata: Array; + /** + * A single key from public metadata. + * + * Tip: Use GraphQL aliases to fetch multiple keys. + */ + metafield: Maybe; + /** Public metadata. Use `keys` to control which fields you want to include. The default is to include everything. */ + metafields: Maybe; + /** List of private metadata items. Requires staff permissions to access. */ + privateMetadata: Array; + /** + * A single key from private metadata. Requires staff permissions to access. + * + * Tip: Use GraphQL aliases to fetch multiple keys. + */ + privateMetafield: Maybe; + /** Private metadata. Requires staff permissions to access. Use `keys` to control which fields you want to include. The default is to include everything. */ + privateMetafields: Maybe; + /** Product variant assigned to digital content. */ + productVariant: ProductVariant; + /** Number of days the URL for the digital content remains valid. */ + urlValidDays: Maybe; + /** List of URLs for the digital variant. */ + urls: Maybe>; + /** Default settings indicator for digital content. */ + useDefaultSettings: Scalars["Boolean"]["output"]; + }; + +/** Represents digital content associated with a product variant. */ +export type DigitalContentMetafieldArgs = { + key: Scalars["String"]["input"]; +}; + +/** Represents digital content associated with a product variant. */ +export type DigitalContentMetafieldsArgs = { + keys?: InputMaybe>; +}; + +/** Represents digital content associated with a product variant. */ +export type DigitalContentPrivateMetafieldArgs = { + key: Scalars["String"]["input"]; +}; + +/** Represents digital content associated with a product variant. */ +export type DigitalContentPrivateMetafieldsArgs = { + keys?: InputMaybe>; +}; + +/** A connection to a list of digital content items. */ +export type DigitalContentCountableConnection = { + edges: Array; + /** Pagination data for this connection. */ + pageInfo: PageInfo; + /** A total count of items in the collection. */ + totalCount: Maybe; +}; + +export type DigitalContentCountableEdge = { + /** A cursor for use in pagination. */ + cursor: Scalars["String"]["output"]; + /** The item at the end of the edge. */ + node: DigitalContent; +}; + /** - * Represents a delivery option for the checkout. + * Create new digital content. This mutation must be sent as a `multipart` request. More detailed specs of the upload format can be found here: https://github.com/jaydenseric/graphql-multipart-request-spec * - * Added in Saleor 3.23. + * Requires one of the following permissions: MANAGE_PRODUCTS. */ -export type Delivery = { - /** The ID of the delivery. */ - id: Scalars["ID"]["output"]; - /** Shipping method represented by the delivery. */ - shippingMethod: Maybe; +export type DigitalContentCreate = { + content: Maybe; + errors: Array; + /** @deprecated Use `errors` field instead. */ + productErrors: Array; + variant: Maybe; }; -/** Represents a delivery method chosen for the checkout. `Warehouse` type is used when checkout is marked as "click and collect" and `ShippingMethod` otherwise. */ -export type DeliveryMethod = ShippingMethod | Warehouse; - /** - * Calculates available delivery options for a checkout. + * Remove digital content assigned to given variant. * - * Added in Saleor 3.23. + * Requires one of the following permissions: MANAGE_PRODUCTS. + */ +export type DigitalContentDelete = { + errors: Array; + /** @deprecated Use `errors` field instead. */ + productErrors: Array; + variant: Maybe; +}; + +export type DigitalContentInput = { + /** Overwrite default automatic_fulfillment setting for variant. */ + automaticFulfillment?: InputMaybe; + /** Determines how many times a download link can be accessed by a customer. */ + maxDownloads?: InputMaybe; + /** + * Fields required to update the digital content metadata. Can be read by any API client authorized to read the object it's attached to. + * + * Warning: never store sensitive information, including financial data such as credit card details. + */ + metadata?: InputMaybe>; + /** + * Fields required to update the digital content private metadata. Requires permissions to modify and to read the metadata of the object it's attached to. + * + * Warning: never store sensitive information, including financial data such as credit card details. + */ + privateMetadata?: InputMaybe>; + /** Determines for how many days a download link is active since it was generated. */ + urlValidDays?: InputMaybe; + /** Use default digital content settings for this product. */ + useDefaultSettings: Scalars["Boolean"]["input"]; +}; + +/** + * Updates digital content. * - * Triggers the following webhook events: - * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered to fetch external shipping methods. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Triggered to filter shipping methods. + * Requires one of the following permissions: MANAGE_PRODUCTS. */ -export type DeliveryOptionsCalculate = { - /** List of the available deliveries. */ - deliveries: Array; - errors: Array; +export type DigitalContentUpdate = { + content: Maybe; + errors: Array; + /** @deprecated Use `errors` field instead. */ + productErrors: Array; + variant: Maybe; }; -export type DeliveryOptionsCalculateError = { - /** The error code. */ - code: DeliveryOptionsCalculateErrorCode; - /** Name of a field that caused the error. A value of `null` indicates that the error isn't associated with a particular field. */ - field: Maybe; - /** The error message. */ - message: Maybe; +export type DigitalContentUploadInput = { + /** Overwrite default automatic_fulfillment setting for variant. */ + automaticFulfillment?: InputMaybe; + /** Represents an file in a multipart request. */ + contentFile: Scalars["Upload"]["input"]; + /** Determines how many times a download link can be accessed by a customer. */ + maxDownloads?: InputMaybe; + /** + * Fields required to update the digital content metadata. Can be read by any API client authorized to read the object it's attached to. + * + * Warning: never store sensitive information, including financial data such as credit card details. + */ + metadata?: InputMaybe>; + /** + * Fields required to update the digital content private metadata. Requires permissions to modify and to read the metadata of the object it's attached to. + * + * Warning: never store sensitive information, including financial data such as credit card details. + */ + privateMetadata?: InputMaybe>; + /** Determines for how many days a download link is active since it was generated. */ + urlValidDays?: InputMaybe; + /** Use default digital content settings for this product. */ + useDefaultSettings: Scalars["Boolean"]["input"]; }; -export type DeliveryOptionsCalculateErrorCode = - | "GRAPHQL_ERROR" - | "INVALID" - | "NOT_FOUND"; +/** Represents a URL for digital content. */ +export type DigitalContentUrl = Node & { + /** Digital content associated with the URL. */ + content: DigitalContent; + /** Date and time when the digital content URL was created. */ + created: Scalars["DateTime"]["output"]; + /** Number of times digital content has been downloaded. */ + downloadNum: Scalars["Int"]["output"]; + /** The ID of the digital content URL. */ + id: Scalars["ID"]["output"]; + /** UUID of digital content. */ + token: Scalars["UUID"]["output"]; + /** URL for digital content. */ + url: Maybe; +}; + +/** + * Generate new URL to digital content. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + */ +export type DigitalContentUrlCreate = { + digitalContentUrl: Maybe; + errors: Array; + /** @deprecated Use `errors` field instead. */ + productErrors: Array; +}; + +export type DigitalContentUrlCreateInput = { + /** Digital content ID which URL will belong to. */ + content: Scalars["ID"]["input"]; +}; export type DiscountError = { /** List of channels IDs which causes the error. */ @@ -7092,11 +7334,7 @@ export type DraftOrderCreateInput = { user?: InputMaybe; /** Email address of the customer. */ userEmail?: InputMaybe; - /** - * ID of the voucher associated with the order. - * - * DEPRECATED: this field will be removed. Use `voucherCode` instead. - */ + /** ID of the voucher associated with the order. */ voucher?: InputMaybe; /** * A code of the voucher associated with the order. @@ -7205,11 +7443,7 @@ export type DraftOrderInput = { user?: InputMaybe; /** Email address of the customer. */ userEmail?: InputMaybe; - /** - * ID of the voucher associated with the order. - * - * DEPRECATED: this field will be removed. Use `voucherCode` instead. - */ + /** ID of the voucher associated with the order. */ voucher?: InputMaybe; /** * A code of the voucher associated with the order. @@ -7620,6 +7854,8 @@ export type ExportScope = * * Added in Saleor 3.18. * + * Note: this API is currently in Feature Preview and can be subject to changes at later point. + * * Requires one of the following permissions: MANAGE_DISCOUNTS. * * Triggers the following webhook events: @@ -8096,7 +8332,7 @@ export type GiftCard = Node & */ endDate: Maybe; /** - * List of events associated with the gift card. Requires MANAGE_GIFT_CARD permission to access all events. Users with MANAGE_ORDERS permission can access only USED_IN_ORDER and REFUNDED_IN_ORDER events. + * List of events associated with the gift card. Requires MANAGE_GIFT_CARD permission to access all events. Users with MANAGE_ORDERS permission can access only USED_IN_ORDER events. * * Requires one of the following permissions: MANAGE_GIFT_CARD, MANAGE_ORDERS. */ @@ -8513,7 +8749,6 @@ export type GiftCardEventsEnum = | "EXPIRY_DATE_UPDATED" | "ISSUED" | "NOTE_ADDED" - | "REFUNDED_IN_ORDER" | "RESENT" | "SENT_TO_CUSTOMER" | "TAGS_UPDATED" @@ -8562,55 +8797,6 @@ export type GiftCardMetadataUpdated = Event & { version: Maybe; }; -/** - * Represents a gift card payment method used for a transaction. - * - * Added in Saleor 3.23. - */ -export type GiftCardPaymentMethodDetails = PaymentMethodDetails & { - /** - * Brand of the gift card. - * - * Added in Saleor 3.23. - */ - brand: Maybe; - /** - * Indicates whether the gift card is a built-in Saleor gift card. - * - * Added in Saleor 3.23. - */ - isSaleorGiftcard: Scalars["Boolean"]["output"]; - /** - * Last characters of the gift card code. Max 4 characters. - * - * Added in Saleor 3.23. - */ - lastChars: Maybe; - /** Name of the gift card. */ - name: Scalars["String"]["output"]; -}; - -export type GiftCardPaymentMethodDetailsInput = { - /** - * Brand of the gift card used for the transaction. Max length is 40 characters. - * - * Added in Saleor 3.23. - */ - brand?: InputMaybe; - /** - * Last characters of the gift card used for the transaction. Max length is 4 characters. - * - * Added in Saleor 3.23. - */ - lastChars?: InputMaybe; - /** - * Name of the payment method used for the transaction. Max length is 256 characters. - * - * Added in Saleor 3.23. - */ - name: Scalars["String"]["input"]; -}; - /** * Resend a gift card. * @@ -8701,8 +8887,6 @@ export type GiftCardSortField = | "CURRENT_BALANCE" /** Sort gift cards by product. */ | "PRODUCT" - /** Sort gift cards by rank. Note: This option is available only with the `search` filter. */ - | "RANK" /** Sort gift cards by used by. */ | "USED_BY"; @@ -8867,6 +9051,8 @@ export type GroupCountableEdge = { node: Group; }; +export type HttpMethod = "GET" | "POST"; + /** Thumbnail formats for icon images. */ export type IconThumbnailFormatEnum = "ORIGINAL" | "WEBP"; @@ -12066,7 +12252,6 @@ export type Mutation = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ checkoutDeliveryMethodUpdate: Maybe; @@ -12134,7 +12319,6 @@ export type Mutation = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout shipping method with the external one. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. * @deprecated Use `checkoutDeliveryMethodUpdate` instead. */ @@ -12278,15 +12462,33 @@ export type Mutation = { */ deleteWarehouse: Maybe; /** - * Calculates available delivery options for a checkout. + * Create new digital content. This mutation must be sent as a `multipart` request. More detailed specs of the upload format can be found here: https://github.com/jaydenseric/graphql-multipart-request-spec * - * Added in Saleor 3.23. + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContentCreate: Maybe; + /** + * Remove digital content assigned to given variant. * - * Triggers the following webhook events: - * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered to fetch external shipping methods. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Triggered to filter shipping methods. + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContentDelete: Maybe; + /** + * Updates digital content. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. */ - deliveryOptionsCalculate: Maybe; + digitalContentUpdate: Maybe; + /** + * Generate new URL to digital content. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContentUrlCreate: Maybe; /** * Deletes draft orders. * @@ -12338,7 +12540,6 @@ export type Mutation = { * Triggers the following webhook events: * - NOTIFY_USER (async): A notification for the exported file. * - GIFT_CARD_EXPORT_COMPLETED (async): A notification for the exported file. - * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportGiftCards: Maybe; /** @@ -12349,7 +12550,6 @@ export type Mutation = { * Triggers the following webhook events: * - NOTIFY_USER (async): A notification for the exported file. * - PRODUCT_EXPORT_COMPLETED (async): A notification for the exported file. - * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportProducts: Maybe; /** @@ -12357,11 +12557,12 @@ export type Mutation = { * * Added in Saleor 3.18. * + * Note: this API is currently in Feature Preview and can be subject to changes at later point. + * * Requires one of the following permissions: MANAGE_DISCOUNTS. * * Triggers the following webhook events: * - VOUCHER_CODE_EXPORT_COMPLETED (async): A notification for the exported file. - * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportVoucherCodes: Maybe; /** Prepare external authentication URL for user by custom plugin. */ @@ -14368,8 +14569,22 @@ export type MutationDeleteWarehouseArgs = { id: Scalars["ID"]["input"]; }; -export type MutationDeliveryOptionsCalculateArgs = { - id: Scalars["ID"]["input"]; +export type MutationDigitalContentCreateArgs = { + input: DigitalContentUploadInput; + variantId: Scalars["ID"]["input"]; +}; + +export type MutationDigitalContentDeleteArgs = { + variantId: Scalars["ID"]["input"]; +}; + +export type MutationDigitalContentUpdateArgs = { + input: DigitalContentInput; + variantId: Scalars["ID"]["input"]; +}; + +export type MutationDigitalContentUrlCreateArgs = { + input: DigitalContentUrlCreateInput; }; export type MutationDraftOrderBulkDeleteArgs = { @@ -15498,6 +15713,15 @@ export type NavigationType = /** Secondary storefront navigation. */ | "SECONDARY"; +/** Represents the NEW_TAB target options for an app extension. */ +export type NewTabTargetOptions = { + /** + * HTTP method for New Tab target (GET or POST) + * @deprecated Use `settings` field directly. + */ + method: HttpMethod; +}; + /** An object with an ID */ export type Node = { /** The ID of the object. */ @@ -17125,6 +17349,7 @@ export type OrderLine = Node & * Requires one of the following permissions: MANAGE_PRODUCTS, MANAGE_ORDERS. */ allocations: Maybe>; + digitalContentUrl: Maybe; /** * List of applied discounts * @@ -18275,8 +18500,6 @@ export type PageSortField = | "PUBLICATION_DATE" /** Sort pages by publication date. */ | "PUBLISHED_AT" - /** Sort pages by rank. Note: This option is available only with the `search` filter. */ - | "RANK" /** Sort pages by slug. */ | "SLUG" /** Sort pages by title. */ @@ -18611,7 +18834,7 @@ export type PageTypeUpdateInput = { addAttributes?: InputMaybe>; /** Name of the page type. */ name?: InputMaybe; - /** List of attribute IDs to be unassigned from the page type. */ + /** List of attribute IDs to be assigned to the page type. */ removeAttributes?: InputMaybe>; /** Page type slug. */ slug?: InputMaybe; @@ -18686,18 +18909,6 @@ export type PasswordChange = { user: Maybe; }; -/** - * Controls whether password-based authentication is allowed. - * - * ENABLED - any user can log in with a password. This is the default behavior. - * CUSTOMERS_ONLY - only customer users can log in with a password. - * If a staff user logs in with a password, they will be treated as a customer - * — the issued token will not contain any staff permissions. - * DISABLED - no user can log in with a password. - * - */ -export type PasswordLoginModeEnum = "CUSTOMERS_ONLY" | "DISABLED" | "ENABLED"; - /** Represents a payment of a given type. */ export type Payment = Node & ObjectWithMetadata & { @@ -18755,6 +18966,11 @@ export type Payment = Node & modified: Scalars["DateTime"]["output"]; /** Order associated with a payment. */ order: Maybe; + /** + * Informs whether this is a partial payment. + * @deprecated This field is reserved for the Adyen Gateway plugin. For other gateways, its value is always `false`. This field will be removed in 3.23 along with the plugin. + */ + partial: Scalars["Boolean"]["output"]; /** Type of method used for payment. */ paymentMethodType: Scalars["String"]["output"]; /** List of private metadata items. Requires staff permissions to access. */ @@ -19158,19 +19374,13 @@ export type PaymentMethodDetailsFilterInput = { }; /** - * Details of the payment method used for the transaction. One of `card`, `other`, or `giftCard` is required. + * Details of the payment method used for the transaction. One of `card` or `other` is required. * * Added in Saleor 3.22. */ export type PaymentMethodDetailsInput = { /** Details of the card payment method used for the transaction. */ card?: InputMaybe; - /** - * Details of the gift card payment method used for the transaction. - * - * Added in Saleor 3.23. - */ - giftCard?: InputMaybe; /** Details of the non-card payment method used for this transaction. */ other?: InputMaybe; }; @@ -19316,11 +19526,10 @@ export type PaymentMethodTokenizationResult = * The following types are possible: * CARD - represents a card payment method. * OTHER - represents any payment method that is not a card payment. - * GIFT_CARD - represents a gift card payment method. * * */ -export type PaymentMethodTypeEnum = "CARD" | "GIFT_CARD" | "OTHER"; +export type PaymentMethodTypeEnum = "CARD" | "OTHER"; export type PaymentMethodTypeEnumFilterInput = { /** The value equal to. */ @@ -20146,6 +20355,7 @@ export type ProductBulkCreateErrorCode = | "ATTRIBUTE_VARIANTS_DISABLED" | "BLANK" | "DUPLICATED_INPUT_ITEM" + | "FILE_SIZE_LIMIT_EXCEEDED" | "GRAPHQL_ERROR" | "INVALID" | "INVALID_PRICE" @@ -20564,6 +20774,7 @@ export type ProductErrorCode = | "ATTRIBUTE_VARIANTS_DISABLED" | "CANNOT_MANAGE_PRODUCT_WITHOUT_VARIANT" | "DUPLICATED_INPUT_ITEM" + | "FILE_SIZE_LIMIT_EXCEEDED" | "GRAPHQL_ERROR" | "INVALID" | "INVALID_FILE_TYPE" @@ -20578,7 +20789,8 @@ export type ProductErrorCode = | "REQUIRED" | "UNIQUE" | "UNSUPPORTED_MEDIA_PROVIDER" - | "UNSUPPORTED_MIME_TYPE"; + | "UNSUPPORTED_MIME_TYPE" + | "VARIANT_NO_DIGITAL_CONTENT"; /** Event sent when product export is completed. */ export type ProductExportCompleted = Event & { @@ -21165,17 +21377,11 @@ export type ProductType = Node & * Requires one of the following permissions: MANAGE_PRODUCTS. */ availableAttributes: Maybe; - /** - * Whether the product type has variants. - * @deprecated This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. - */ + /** Whether the product type has variants. */ hasVariants: Scalars["Boolean"]["output"]; /** The ID of the product type. */ id: Scalars["ID"]["output"]; - /** - * Whether the product type is digital - doesn't have any effect, it's present for backward-compatibility. - * @deprecated Will be removed in v3.24.0, use metadata or attributes instead. - */ + /** Whether the product type is digital. */ isDigital: Scalars["Boolean"]["output"]; /** Whether shipping is required for this product type. */ isShippingRequired: Scalars["Boolean"]["output"]; @@ -21339,11 +21545,6 @@ export type ProductTypeDelete = { export type ProductTypeEnum = "DIGITAL" | "SHIPPABLE"; export type ProductTypeFilterInput = { - /** - * - * - * DEPRECATED: this field will be removed. The field has no effect on the API behavior. This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. - */ configurable?: InputMaybe; ids?: InputMaybe>; kind?: InputMaybe; @@ -21354,13 +21555,9 @@ export type ProductTypeFilterInput = { }; export type ProductTypeInput = { - /** - * Determines if product of this type has multiple variants. This option mainly simplifies product management in the dashboard. There is always at least one variant created under the hood. - * - * DEPRECATED: this field will be removed. The field has no effect on the API behavior. This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. - */ + /** Determines if product of this type has multiple variants. This option mainly simplifies product management in the dashboard. There is always at least one variant created under the hood. */ hasVariants?: InputMaybe; - /** Determines if products are digital - doesn't have any effect, it's present for backward-compatibility. */ + /** Determines if products are digital. */ isDigital?: InputMaybe; /** Determines if shipping is required for products of this variant. */ isShippingRequired?: InputMaybe; @@ -21492,6 +21689,12 @@ export type ProductVariant = Node & channelListings: Maybe>; /** The date and time when the product variant was created. */ created: Scalars["DateTime"]["output"]; + /** + * Digital content for the product variant. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + */ + digitalContent: Maybe; /** External ID of this product. */ externalReference: Maybe; /** The ID of the product variant. */ @@ -21878,9 +22081,7 @@ export type ProductVariantChannelListing = Node & { /** The price of the variant. */ price: Maybe; /** - * Previous price of the variant in channel. Useful for providing promotion information required by customer protection laws such as EU Omnibus directive. - * - * Warning: This field is not updated automatically. Use Channel Listings mutation to update it manually. + * Prior price of the variant used for discount calculations. * * Added in Saleor 3.21. */ @@ -23502,6 +23703,20 @@ export type Query = { * Requires one of the following permissions: MANAGE_ORDERS, MANAGE_USERS. */ customers: Maybe; + /** + * Look up digital content by ID. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContent: Maybe; + /** + * List of digital content. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContents: Maybe; /** * List of draft orders. The query will not initiate any external requests, including filtering available shipping methods, or performing external tax calculations. * @@ -23828,7 +24043,7 @@ export type Query = { }; export type Query_EntitiesArgs = { - representations: Array; + representations?: InputMaybe>>; }; export type QueryAddressArgs = { @@ -23958,6 +24173,17 @@ export type QueryCustomersArgs = { where?: InputMaybe; }; +export type QueryDigitalContentArgs = { + id: Scalars["ID"]["input"]; +}; + +export type QueryDigitalContentsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + export type QueryDraftOrdersArgs = { after?: InputMaybe; before?: InputMaybe; @@ -24304,7 +24530,6 @@ export type QueryTransactionsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; - sortBy?: InputMaybe; where?: InputMaybe; }; @@ -24437,7 +24662,7 @@ export type RefundSettingsErrorCode = "GRAPHQL_ERROR" | "INVALID" | "REQUIRED"; export type RefundSettingsUpdate = { errors: Array; /** Refund settings. */ - refundSettings: Maybe; + refundSettings: RefundSettings; /** @deprecated Use `errors` field instead. */ refundSettingsErrors: Array; }; @@ -25168,10 +25393,7 @@ export type ShippingMethod = Node & id: Scalars["ID"]["output"]; /** Maximum delivery days for this shipping method. */ maximumDeliveryDays: Maybe; - /** - * Maximum order price for this shipping method. - * @deprecated No longer supported - */ + /** Maximum order price for this shipping method. */ maximumOrderPrice: Maybe; /** * Maximum order weight for this shipping method. @@ -25192,10 +25414,7 @@ export type ShippingMethod = Node & metafields: Maybe; /** Minimum delivery days for this shipping method. */ minimumDeliveryDays: Maybe; - /** - * Minimal order price for this shipping method. - * @deprecated No longer supported - */ + /** Minimal order price for this shipping method. */ minimumOrderPrice: Maybe; /** * Minimum order weight for this shipping method. @@ -25942,6 +26161,12 @@ export type Shop = ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_SETTINGS. */ allowLoginWithoutConfirmation: Maybe; + /** + * Enable automatic fulfillment for all digital products. + * + * Requires one of the following permissions: MANAGE_SETTINGS. + */ + automaticFulfillmentDigitalProducts: Maybe; /** List of available external authentications. */ availableExternalAuthentications: Array; /** List of available payment gateways. */ @@ -25975,6 +26200,18 @@ export type Shop = ObjectWithMetadata & { customerSetPasswordUrl: Maybe; /** Shop's default country. */ defaultCountry: Maybe; + /** + * Default number of max downloads per digital content URL. + * + * Requires one of the following permissions: MANAGE_SETTINGS. + */ + defaultDigitalMaxDownloads: Maybe; + /** + * Default number of days which digital content URL will be valid. + * + * Requires one of the following permissions: MANAGE_SETTINGS. + */ + defaultDigitalUrlValidDays: Maybe; /** * Default shop's email sender's address. * @@ -26044,12 +26281,6 @@ export type Shop = ObjectWithMetadata & { metafields: Maybe; /** Shop's name. */ name: Scalars["String"]["output"]; - /** - * Controls whether password-based authentication is allowed. - * - * Added in Saleor 3.23. - */ - passwordLoginMode: PasswordLoginModeEnum; /** List of available permissions. */ permissions: Array; /** List of possible phone prefixes. */ @@ -26096,12 +26327,6 @@ export type Shop = ObjectWithMetadata & { trackInventoryByDefault: Maybe; /** Returns translated shop fields for the given language code. */ translation: Maybe; - /** - * When enabled, stock availability is filtered by shipping zones and the destination address (legacy behavior). When disabled, stock availability is determined only by the direct warehouse-channel link, ignoring shipping zones. - * - * Added in Saleor 3.23. - */ - useLegacyShippingZoneStockAvailability: Scalars["Boolean"]["output"]; /** * Use legacy update webhook emission. When enabled, update webhooks (e.g. `customerUpdated`,`productVariantUpdated`) are sent even when only metadata changes. When disabled, update webhooks are not sent for metadata-only changes; only metadata-specific webhooks (e.g., `customerMetadataUpdated`, `productVariantMetadataUpdated`) are sent. * @@ -26201,7 +26426,6 @@ export type ShopErrorCode = | "GRAPHQL_ERROR" | "INVALID" | "NOT_FOUND" - | "PASSWORD_AUTH_RESTRICTION" | "REQUIRED" | "UNIQUE"; @@ -26235,6 +26459,8 @@ export type ShopMetadataUpdated = Event & { export type ShopSettingsInput = { /** Enable possibility to login without account confirmation. */ allowLoginWithoutConfirmation?: InputMaybe; + /** Enable automatic fulfillment for all digital products. */ + automaticFulfillmentDigitalProducts?: InputMaybe; /** * Charge taxes on shipping. * @@ -26243,6 +26469,10 @@ export type ShopSettingsInput = { chargeTaxesOnShipping?: InputMaybe; /** URL of a view where customers can set their password. */ customerSetPasswordUrl?: InputMaybe; + /** Default number of max downloads per digital content URL. */ + defaultDigitalMaxDownloads?: InputMaybe; + /** Default number of days which digital content URL will be valid. */ + defaultDigitalUrlValidDays?: InputMaybe; /** Default email sender's address. */ defaultMailSenderAddress?: InputMaybe; /** Default email sender's name. */ @@ -26279,12 +26509,6 @@ export type ShopSettingsInput = { * Warning: never store sensitive information, including financial data such as credit card details. */ metadata?: InputMaybe>; - /** - * Controls whether password-based authentication is allowed. - * - * Added in Saleor 3.23. - */ - passwordLoginMode?: InputMaybe; /** * When enabled, address fields that are not valid for a given country (according to Google's i18n address data) will be preserved instead of being removed during validation. Validation errors are still returned. * @@ -26303,14 +26527,6 @@ export type ShopSettingsInput = { reserveStockDurationAuthenticatedUser?: InputMaybe; /** This field is used as a default value for `ProductVariant.trackInventory`. */ trackInventoryByDefault?: InputMaybe; - /** - * When enabled, stock availability is filtered by shipping zones and the destination address (legacy behavior). When disabled, stock availability is determined only by the direct warehouse-channel link, ignoring shipping zones. - * - * Added in Saleor 3.23. - */ - useLegacyShippingZoneStockAvailability?: InputMaybe< - Scalars["Boolean"]["input"] - >; /** * Use legacy update webhook emission. When enabled, update webhooks (e.g. `customerUpdated`,`productVariantUpdated`) are sent even when only metadata changes. When disabled, update webhooks are not sent for metadata-only changes; only metadata-specific webhooks (e.g., `customerMetadataUpdated`, `productVariantMetadataUpdated`) are sent. * @@ -27942,26 +28158,6 @@ export type TransactionEvent = Node & { type: Maybe; }; -/** - * Filter input for transaction events data. - * - * Added in Saleor 3.23. - */ -export type TransactionEventFilterInput = { - /** - * Filter transaction events by created at date. - * - * Added in Saleor 3.23. - */ - createdAt?: InputMaybe; - /** - * Filter transaction events by type. - * - * Added in Saleor 3.23. - */ - type?: InputMaybe; -}; - export type TransactionEventInput = { /** The message related to the event. */ message?: InputMaybe; @@ -28054,13 +28250,6 @@ export type TransactionEventTypeEnum = | "REFUND_REVERSE" | "REFUND_SUCCESS"; -export type TransactionEventTypeEnumFilterInput = { - /** The value equal to. */ - eq?: InputMaybe; - /** The value included in. */ - oneOf?: InputMaybe>; -}; - /** Filter input for transactions. */ export type TransactionFilterInput = { /** Filter by metadata fields of transactions. */ @@ -28405,27 +28594,6 @@ export type TransactionRequestRefundForGrantedRefundErrorCode = | "REFUND_ALREADY_PROCESSED" | "REFUND_IS_PENDING"; -export type TransactionSortField = - /** - * Sort transactions by creation date. - * - * Added in Saleor 3.23. - */ - | "CREATED_AT" - /** - * Sort transactions by modification date. - * - * Added in Saleor 3.23. - */ - | "MODIFIED_AT"; - -export type TransactionSortingInput = { - /** Specifies the direction in which to sort transactions. */ - direction: OrderDirection; - /** Sort transactions by the selected field. */ - field: TransactionSortField; -}; - /** * Update transaction. * @@ -28499,25 +28667,7 @@ export type TransactionWhereInput = { OR?: InputMaybe>; /** Filter by app identifier. */ appIdentifier?: InputMaybe; - /** - * Filter transactions by created at date. - * - * Added in Saleor 3.23. - */ - createdAt?: InputMaybe; - /** - * Filter by transaction events. Each list item represents conditions that must be satisfied by a single event. The filter matches transactions that have related events meeting all specified groups of conditions. - * - * Added in Saleor 3.23. - */ - events?: InputMaybe>; ids?: InputMaybe>; - /** - * Filter transactions by modified at date. - * - * Added in Saleor 3.23. - */ - modifiedAt?: InputMaybe; /** Filter by PSP reference. */ pspReference?: InputMaybe; }; @@ -29005,9 +29155,7 @@ export type UserSortField = /** Sort users by last name. */ | "LAST_NAME" /** Sort users by order count. */ - | "ORDER_COUNT" - /** Sort users by rank. Note: This option is available only with the `search` filter. */ - | "RANK"; + | "ORDER_COUNT"; export type UserSortingInput = { /** Specifies the direction in which to sort users. */ @@ -31188,6 +31336,15 @@ export type Weight = { export type WeightUnitsEnum = "G" | "KG" | "LB" | "OZ" | "TONNE"; +/** Represents the WIDGET target options for an app extension. */ +export type WidgetTargetOptions = { + /** + * HTTP method for Widget target (GET or POST) + * @deprecated Use `settings` field directly. + */ + method: HttpMethod; +}; + /** _Entity union as defined by Federation spec. */ export type _Entity = | Address diff --git a/packages/codegen/schema.ts b/packages/codegen/schema.ts index a03bf50c..e7c579cc 100644 --- a/packages/codegen/schema.ts +++ b/packages/codegen/schema.ts @@ -252,8 +252,8 @@ export type AccountErrorCode = | 'DELETE_OWN_ACCOUNT' | 'DELETE_STAFF_ACCOUNT' | 'DELETE_SUPERUSER_ACCOUNT' - | 'DISABLED_AUTHENTICATION_METHOD' | 'DUPLICATED_INPUT_ITEM' + | 'FILE_SIZE_LIMIT_EXCEEDED' | 'GRAPHQL_ERROR' | 'INACTIVE' | 'INVALID' @@ -1043,21 +1043,38 @@ export type AppExtension = Node & { /** Label of the extension to show in the dashboard. */ label: Scalars['String']['output']; /** - * Name of the extension mount point in the dashboard. Value returned in UPPERCASE. + * Place where given extension will be mounted. + * @deprecated Use `mountName` instead. + */ + mount: AppExtensionMountEnum; + /** + * Name of the extension mount point in the dashboard. Replaces `mount` * * Added in Saleor 3.22. */ mountName: Scalars['String']['output']; + /** + * App extension options. + * + * Added in Saleor 3.22. + * @deprecated Use `settings` field instead. + */ + options: Maybe; /** List of the app extension's permissions. */ permissions: Array; /** - * App extension settings. + * App extension settings. Replaces `options` field. * * Added in Saleor 3.22. */ settings: Scalars['JSON']['output']; /** - * Name of the extension target in the dashboard. Value returned in UPPERCASE. + * Type of way how app extension will be opened. + * @deprecated Use `targetName` instead. + */ + target: AppExtensionTargetEnum; + /** + * Name of the extension target in the dashboard. Replaces `target` * * Added in Saleor 3.22. */ @@ -1082,12 +1099,24 @@ export type AppExtensionCountableEdge = { }; export type AppExtensionFilterInput = { + /** + * DEPRECATED: Use `mountName` instead. + * + * DEPRECATED: this field will be removed. + */ + mount?: InputMaybe>; /** * Plain-text mount name (case insensitive) * * Added in Saleor 3.22. */ mountName?: InputMaybe>; + /** + * DEPRECATED: Use `targetName` instead. + * + * DEPRECATED: this field will be removed. + */ + target?: InputMaybe; /** * Plain-text target name (case insensitive) * @@ -1096,6 +1125,92 @@ export type AppExtensionFilterInput = { targetName?: InputMaybe; }; +/** All places where app extension can be mounted. */ +export type AppExtensionMountEnum = + | 'CATEGORY_DETAILS_MORE_ACTIONS' + | 'CATEGORY_OVERVIEW_CREATE' + | 'CATEGORY_OVERVIEW_MORE_ACTIONS' + | 'COLLECTION_DETAILS_MORE_ACTIONS' + | 'COLLECTION_DETAILS_WIDGETS' + | 'COLLECTION_OVERVIEW_CREATE' + | 'COLLECTION_OVERVIEW_MORE_ACTIONS' + | 'CUSTOMER_DETAILS_MORE_ACTIONS' + | 'CUSTOMER_DETAILS_WIDGETS' + | 'CUSTOMER_OVERVIEW_CREATE' + | 'CUSTOMER_OVERVIEW_MORE_ACTIONS' + | 'DISCOUNT_DETAILS_MORE_ACTIONS' + | 'DISCOUNT_OVERVIEW_CREATE' + | 'DISCOUNT_OVERVIEW_MORE_ACTIONS' + | 'DRAFT_ORDER_DETAILS_MORE_ACTIONS' + | 'DRAFT_ORDER_DETAILS_WIDGETS' + | 'DRAFT_ORDER_OVERVIEW_CREATE' + | 'DRAFT_ORDER_OVERVIEW_MORE_ACTIONS' + | 'GIFT_CARD_DETAILS_MORE_ACTIONS' + | 'GIFT_CARD_DETAILS_WIDGETS' + | 'GIFT_CARD_OVERVIEW_CREATE' + | 'GIFT_CARD_OVERVIEW_MORE_ACTIONS' + | 'MENU_DETAILS_MORE_ACTIONS' + | 'MENU_OVERVIEW_CREATE' + | 'MENU_OVERVIEW_MORE_ACTIONS' + | 'NAVIGATION_CATALOG' + | 'NAVIGATION_CUSTOMERS' + | 'NAVIGATION_DISCOUNTS' + | 'NAVIGATION_ORDERS' + | 'NAVIGATION_PAGES' + | 'NAVIGATION_TRANSLATIONS' + | 'ORDER_DETAILS_MORE_ACTIONS' + | 'ORDER_DETAILS_WIDGETS' + | 'ORDER_OVERVIEW_CREATE' + | 'ORDER_OVERVIEW_MORE_ACTIONS' + | 'PAGE_DETAILS_MORE_ACTIONS' + | 'PAGE_OVERVIEW_CREATE' + | 'PAGE_OVERVIEW_MORE_ACTIONS' + | 'PAGE_TYPE_DETAILS_MORE_ACTIONS' + | 'PAGE_TYPE_OVERVIEW_CREATE' + | 'PAGE_TYPE_OVERVIEW_MORE_ACTIONS' + | 'PRODUCT_DETAILS_MORE_ACTIONS' + | 'PRODUCT_DETAILS_WIDGETS' + | 'PRODUCT_OVERVIEW_CREATE' + | 'PRODUCT_OVERVIEW_MORE_ACTIONS' + | 'TRANSLATIONS_MORE_ACTIONS' + | 'VOUCHER_DETAILS_MORE_ACTIONS' + | 'VOUCHER_DETAILS_WIDGETS' + | 'VOUCHER_OVERVIEW_CREATE' + | 'VOUCHER_OVERVIEW_MORE_ACTIONS'; + +/** Represents the options for an app extension. */ +export type AppExtensionOptionsNewTab = { + /** + * Options controlling behavior of the NEW_TAB extension target + * @deprecated Use `settings` field directly. + */ + newTabTarget: Maybe; +}; + +/** Represents the options for an app extension. */ +export type AppExtensionOptionsWidget = { + /** + * Options for displaying a Widget + * @deprecated Use `settings` field directly. + */ + widgetTarget: Maybe; +}; + +export type AppExtensionPossibleOptions = AppExtensionOptionsNewTab | AppExtensionOptionsWidget; + +/** + * All available ways of opening an app extension. + * + * POPUP - app's extension will be mounted as a popup window + * APP_PAGE - redirect to app's page + * + */ +export type AppExtensionTargetEnum = + | 'APP_PAGE' + | 'NEW_TAB' + | 'POPUP' + | 'WIDGET'; + /** * Fetch and validate manifest. * @@ -1140,9 +1255,9 @@ export type AppInstallInput = { /** Determine if app will be set active or not. */ activateAfterInstallation?: InputMaybe; /** Name of the app to install. */ - appName: Scalars['String']['input']; + appName?: InputMaybe; /** URL to app's manifest in JSON format. */ - manifestUrl: Scalars['String']['input']; + manifestUrl?: InputMaybe; /** List of permission code names to assign to this app. */ permissions?: InputMaybe>; }; @@ -1204,7 +1319,12 @@ export type AppManifestExtension = { /** Label of the extension to show in the dashboard. */ label: Scalars['String']['output']; /** - * Name of the extension mount point in the dashboard. Value returned in UPPERCASE. + * Place where given extension will be mounted. + * @deprecated Use `mountName` instead. + */ + mount: AppExtensionMountEnum; + /** + * Name of the extension mount point in the dashboard. Replaces `mount` * * Added in Saleor 3.22. */ @@ -1212,13 +1332,18 @@ export type AppManifestExtension = { /** List of the app extension's permissions. */ permissions: Array; /** - * App extension settings. + * JSON object with settings for this extension. * * Added in Saleor 3.22. */ settings: Scalars['JSON']['output']; /** - * Name of the extension target in the dashboard. Value returned in UPPERCASE. + * Type of way how app extension will be opened. + * @deprecated Use `targetName` instead. + */ + target: AppExtensionTargetEnum; + /** + * Name of the extension target in the dashboard. Replaces `target` * * Added in Saleor 3.22. */ @@ -2061,7 +2186,7 @@ export type Attribute = Node & ObjectWithMetadata & { /** Public metadata. Use `keys` to control which fields you want to include. The default is to include everything. */ metafields: Maybe; /** Name of an attribute displayed in the interface. */ - name: Scalars['String']['output']; + name: Maybe; /** List of private metadata items. Requires staff permissions to access. */ privateMetadata: Array; /** @@ -2083,7 +2208,7 @@ export type Attribute = Node & ObjectWithMetadata & { */ referenceTypes: Maybe>; /** Internal representation of an attribute name. */ - slug: Scalars['String']['output']; + slug: Maybe; /** * The position of the attribute in the storefront navigation (0 by default). Requires one of the following permissions: MANAGE_PAGES, MANAGE_PAGE_TYPES_AND_ATTRIBUTES, MANAGE_PRODUCTS, MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES. * @deprecated No longer supported @@ -2092,7 +2217,7 @@ export type Attribute = Node & ObjectWithMetadata & { /** Returns translated attribute fields for the given language code. */ translation: Maybe; /** The attribute type. */ - type: AttributeTypeEnum; + type: Maybe; /** The unit of attribute values. */ unit: Maybe; /** Whether the attribute requires values to be passed or not. Requires one of the following permissions: MANAGE_PAGES, MANAGE_PAGE_TYPES_AND_ATTRIBUTES, MANAGE_PRODUCTS, MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES. */ @@ -4227,19 +4352,12 @@ export type Checkout = Node & ObjectWithMetadata & { * Added in Saleor 3.21. */ customerNote: Scalars['String']['output']; - /** - * The delivery method selected for this checkout. - * - * Added in Saleor 3.23. - */ - delivery: Maybe; /** * The delivery method selected for this checkout. * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Optionally triggered when cached external shipping methods are invalid. * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. - * @deprecated Use `delivery` instead. */ deliveryMethod: Maybe; /** The total discount applied to the checkout. Note: Only discount created via voucher are included in this field. */ @@ -4299,7 +4417,7 @@ export type Checkout = Node & ObjectWithMetadata & { * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Optionally triggered when cached external shipping methods are invalid. * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. - * @deprecated Use `delivery` instead. + * @deprecated Use `deliveryMethod` instead. */ shippingMethod: Maybe; /** @@ -4705,7 +4823,6 @@ export type CheckoutCustomerNoteUpdate = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ export type CheckoutDeliveryMethodUpdate = { @@ -5104,25 +5221,7 @@ export type CheckoutPaymentCreate = { }; /** Represents an problem in the checkout. */ -export type CheckoutProblem = CheckoutLineProblemInsufficientStock | CheckoutLineProblemVariantNotAvailable | CheckoutProblemDeliveryMethodInvalid | CheckoutProblemDeliveryMethodStale; - -/** - * Indicates that the selected delivery method is invalid. - * - * Added in Saleor 3.23. - */ -export type CheckoutProblemDeliveryMethodInvalid = { - delivery: Delivery; -}; - -/** - * Indicates that the delivery methods are stale. - * - * Added in Saleor 3.23. - */ -export type CheckoutProblemDeliveryMethodStale = { - delivery: Delivery; -}; +export type CheckoutProblem = CheckoutLineProblemInsufficientStock | CheckoutLineProblemVariantNotAvailable; /** * Remove a gift card or a voucher from a checkout. @@ -5140,12 +5239,6 @@ export type CheckoutRemovePromoCode = { /** Represents the channel-specific checkout settings. */ export type CheckoutSettings = { - /** - * Default to `true`. Determines whether gift cards can be attached to a Checkout via `addPromoCode` mutation. Usage of this mutation with gift cards is deprecated. - * - * Added in Saleor 3.23. - */ - allowLegacyGiftCardUse: Scalars['Boolean']['output']; /** * The date time defines the earliest checkout creation date on which fully paid checkouts can begin to be automatically completed. * @@ -5173,12 +5266,6 @@ export type CheckoutSettings = { }; export type CheckoutSettingsInput = { - /** - * Default to `true`. Determines whether gift cards can be attached to a Checkout via `addPromoCode` mutation. Usage of this mutation with gift cards is deprecated. - * - * Added in Saleor 3.23. - */ - allowLegacyGiftCardUse?: InputMaybe; /** * Settings for automatic completion of fully paid checkouts. * @@ -5220,7 +5307,6 @@ export type CheckoutShippingAddressUpdate = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout shipping method with the external one. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ export type CheckoutShippingMethodUpdate = { @@ -5237,9 +5323,7 @@ export type CheckoutSortField = /** Sort checkouts by customer. */ | 'CUSTOMER' /** Sort checkouts by payment. */ - | 'PAYMENT' - /** Sort checkouts by rank. Note: This option is available only with the `search` filter. */ - | 'RANK'; + | 'PAYMENT'; export type CheckoutSortingInput = { /** Specifies the direction in which to sort checkouts. */ @@ -5606,6 +5690,7 @@ export type CollectionError = { export type CollectionErrorCode = | 'CANNOT_MANAGE_PRODUCT_WITHOUT_VARIANT' | 'DUPLICATED_INPUT_ITEM' + | 'FILE_SIZE_LIMIT_EXCEEDED' | 'GRAPHQL_ERROR' | 'INVALID' | 'NOT_FOUND' @@ -6663,6 +6748,8 @@ export type CustomerEvent = Node & { message: Maybe; /** The concerned order. */ order: Maybe; + /** The concerned order line. */ + orderLine: Maybe; /** Customer event type. */ type: Maybe; /** User who performed the action. */ @@ -6928,49 +7015,207 @@ export type DeletePrivateMetadata = { metadataErrors: Array; }; +/** Represents a delivery method chosen for the checkout. `Warehouse` type is used when checkout is marked as "click and collect" and `ShippingMethod` otherwise. */ +export type DeliveryMethod = ShippingMethod | Warehouse; + +/** Represents digital content associated with a product variant. */ +export type DigitalContent = Node & ObjectWithMetadata & { + /** Indicator for automatic fulfillment of digital content. */ + automaticFulfillment: Scalars['Boolean']['output']; + /** File associated with digital content. */ + contentFile: Scalars['String']['output']; + /** The ID of the digital content. */ + id: Scalars['ID']['output']; + /** Maximum number of allowed downloads for the digital content. */ + maxDownloads: Maybe; + /** List of public metadata items. Can be accessed without permissions. */ + metadata: Array; + /** + * A single key from public metadata. + * + * Tip: Use GraphQL aliases to fetch multiple keys. + */ + metafield: Maybe; + /** Public metadata. Use `keys` to control which fields you want to include. The default is to include everything. */ + metafields: Maybe; + /** List of private metadata items. Requires staff permissions to access. */ + privateMetadata: Array; + /** + * A single key from private metadata. Requires staff permissions to access. + * + * Tip: Use GraphQL aliases to fetch multiple keys. + */ + privateMetafield: Maybe; + /** Private metadata. Requires staff permissions to access. Use `keys` to control which fields you want to include. The default is to include everything. */ + privateMetafields: Maybe; + /** Product variant assigned to digital content. */ + productVariant: ProductVariant; + /** Number of days the URL for the digital content remains valid. */ + urlValidDays: Maybe; + /** List of URLs for the digital variant. */ + urls: Maybe>; + /** Default settings indicator for digital content. */ + useDefaultSettings: Scalars['Boolean']['output']; +}; + + +/** Represents digital content associated with a product variant. */ +export type DigitalContentMetafieldArgs = { + key: Scalars['String']['input']; +}; + + +/** Represents digital content associated with a product variant. */ +export type DigitalContentMetafieldsArgs = { + keys?: InputMaybe>; +}; + + +/** Represents digital content associated with a product variant. */ +export type DigitalContentPrivateMetafieldArgs = { + key: Scalars['String']['input']; +}; + + +/** Represents digital content associated with a product variant. */ +export type DigitalContentPrivateMetafieldsArgs = { + keys?: InputMaybe>; +}; + +/** A connection to a list of digital content items. */ +export type DigitalContentCountableConnection = { + edges: Array; + /** Pagination data for this connection. */ + pageInfo: PageInfo; + /** A total count of items in the collection. */ + totalCount: Maybe; +}; + +export type DigitalContentCountableEdge = { + /** A cursor for use in pagination. */ + cursor: Scalars['String']['output']; + /** The item at the end of the edge. */ + node: DigitalContent; +}; + /** - * Represents a delivery option for the checkout. + * Create new digital content. This mutation must be sent as a `multipart` request. More detailed specs of the upload format can be found here: https://github.com/jaydenseric/graphql-multipart-request-spec * - * Added in Saleor 3.23. + * Requires one of the following permissions: MANAGE_PRODUCTS. */ -export type Delivery = { - /** The ID of the delivery. */ - id: Scalars['ID']['output']; - /** Shipping method represented by the delivery. */ - shippingMethod: Maybe; +export type DigitalContentCreate = { + content: Maybe; + errors: Array; + /** @deprecated Use `errors` field instead. */ + productErrors: Array; + variant: Maybe; }; -/** Represents a delivery method chosen for the checkout. `Warehouse` type is used when checkout is marked as "click and collect" and `ShippingMethod` otherwise. */ -export type DeliveryMethod = ShippingMethod | Warehouse; - /** - * Calculates available delivery options for a checkout. + * Remove digital content assigned to given variant. * - * Added in Saleor 3.23. + * Requires one of the following permissions: MANAGE_PRODUCTS. + */ +export type DigitalContentDelete = { + errors: Array; + /** @deprecated Use `errors` field instead. */ + productErrors: Array; + variant: Maybe; +}; + +export type DigitalContentInput = { + /** Overwrite default automatic_fulfillment setting for variant. */ + automaticFulfillment?: InputMaybe; + /** Determines how many times a download link can be accessed by a customer. */ + maxDownloads?: InputMaybe; + /** + * Fields required to update the digital content metadata. Can be read by any API client authorized to read the object it's attached to. + * + * Warning: never store sensitive information, including financial data such as credit card details. + */ + metadata?: InputMaybe>; + /** + * Fields required to update the digital content private metadata. Requires permissions to modify and to read the metadata of the object it's attached to. + * + * Warning: never store sensitive information, including financial data such as credit card details. + */ + privateMetadata?: InputMaybe>; + /** Determines for how many days a download link is active since it was generated. */ + urlValidDays?: InputMaybe; + /** Use default digital content settings for this product. */ + useDefaultSettings: Scalars['Boolean']['input']; +}; + +/** + * Updates digital content. * - * Triggers the following webhook events: - * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered to fetch external shipping methods. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Triggered to filter shipping methods. + * Requires one of the following permissions: MANAGE_PRODUCTS. */ -export type DeliveryOptionsCalculate = { - /** List of the available deliveries. */ - deliveries: Array; - errors: Array; +export type DigitalContentUpdate = { + content: Maybe; + errors: Array; + /** @deprecated Use `errors` field instead. */ + productErrors: Array; + variant: Maybe; }; -export type DeliveryOptionsCalculateError = { - /** The error code. */ - code: DeliveryOptionsCalculateErrorCode; - /** Name of a field that caused the error. A value of `null` indicates that the error isn't associated with a particular field. */ - field: Maybe; - /** The error message. */ - message: Maybe; +export type DigitalContentUploadInput = { + /** Overwrite default automatic_fulfillment setting for variant. */ + automaticFulfillment?: InputMaybe; + /** Represents an file in a multipart request. */ + contentFile: Scalars['Upload']['input']; + /** Determines how many times a download link can be accessed by a customer. */ + maxDownloads?: InputMaybe; + /** + * Fields required to update the digital content metadata. Can be read by any API client authorized to read the object it's attached to. + * + * Warning: never store sensitive information, including financial data such as credit card details. + */ + metadata?: InputMaybe>; + /** + * Fields required to update the digital content private metadata. Requires permissions to modify and to read the metadata of the object it's attached to. + * + * Warning: never store sensitive information, including financial data such as credit card details. + */ + privateMetadata?: InputMaybe>; + /** Determines for how many days a download link is active since it was generated. */ + urlValidDays?: InputMaybe; + /** Use default digital content settings for this product. */ + useDefaultSettings: Scalars['Boolean']['input']; }; -export type DeliveryOptionsCalculateErrorCode = - | 'GRAPHQL_ERROR' - | 'INVALID' - | 'NOT_FOUND'; +/** Represents a URL for digital content. */ +export type DigitalContentUrl = Node & { + /** Digital content associated with the URL. */ + content: DigitalContent; + /** Date and time when the digital content URL was created. */ + created: Scalars['DateTime']['output']; + /** Number of times digital content has been downloaded. */ + downloadNum: Scalars['Int']['output']; + /** The ID of the digital content URL. */ + id: Scalars['ID']['output']; + /** UUID of digital content. */ + token: Scalars['UUID']['output']; + /** URL for digital content. */ + url: Maybe; +}; + +/** + * Generate new URL to digital content. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + */ +export type DigitalContentUrlCreate = { + digitalContentUrl: Maybe; + errors: Array; + /** @deprecated Use `errors` field instead. */ + productErrors: Array; +}; + +export type DigitalContentUrlCreateInput = { + /** Digital content ID which URL will belong to. */ + content: Scalars['ID']['input']; +}; export type DiscountError = { /** List of channels IDs which causes the error. */ @@ -7141,11 +7386,7 @@ export type DraftOrderCreateInput = { user?: InputMaybe; /** Email address of the customer. */ userEmail?: InputMaybe; - /** - * ID of the voucher associated with the order. - * - * DEPRECATED: this field will be removed. Use `voucherCode` instead. - */ + /** ID of the voucher associated with the order. */ voucher?: InputMaybe; /** * A code of the voucher associated with the order. @@ -7254,11 +7495,7 @@ export type DraftOrderInput = { user?: InputMaybe; /** Email address of the customer. */ userEmail?: InputMaybe; - /** - * ID of the voucher associated with the order. - * - * DEPRECATED: this field will be removed. Use `voucherCode` instead. - */ + /** ID of the voucher associated with the order. */ voucher?: InputMaybe; /** * A code of the voucher associated with the order. @@ -7672,6 +7909,8 @@ export type ExportScope = * * Added in Saleor 3.18. * + * Note: this API is currently in Feature Preview and can be subject to changes at later point. + * * Requires one of the following permissions: MANAGE_DISCOUNTS. * * Triggers the following webhook events: @@ -8152,7 +8391,7 @@ export type GiftCard = Node & ObjectWithMetadata & { */ endDate: Maybe; /** - * List of events associated with the gift card. Requires MANAGE_GIFT_CARD permission to access all events. Users with MANAGE_ORDERS permission can access only USED_IN_ORDER and REFUNDED_IN_ORDER events. + * List of events associated with the gift card. Requires MANAGE_GIFT_CARD permission to access all events. Users with MANAGE_ORDERS permission can access only USED_IN_ORDER events. * * Requires one of the following permissions: MANAGE_GIFT_CARD, MANAGE_ORDERS. */ @@ -8574,7 +8813,6 @@ export type GiftCardEventsEnum = | 'EXPIRY_DATE_UPDATED' | 'ISSUED' | 'NOTE_ADDED' - | 'REFUNDED_IN_ORDER' | 'RESENT' | 'SENT_TO_CUSTOMER' | 'TAGS_UPDATED' @@ -8623,55 +8861,6 @@ export type GiftCardMetadataUpdated = Event & { version: Maybe; }; -/** - * Represents a gift card payment method used for a transaction. - * - * Added in Saleor 3.23. - */ -export type GiftCardPaymentMethodDetails = PaymentMethodDetails & { - /** - * Brand of the gift card. - * - * Added in Saleor 3.23. - */ - brand: Maybe; - /** - * Indicates whether the gift card is a built-in Saleor gift card. - * - * Added in Saleor 3.23. - */ - isSaleorGiftcard: Scalars['Boolean']['output']; - /** - * Last characters of the gift card code. Max 4 characters. - * - * Added in Saleor 3.23. - */ - lastChars: Maybe; - /** Name of the gift card. */ - name: Scalars['String']['output']; -}; - -export type GiftCardPaymentMethodDetailsInput = { - /** - * Brand of the gift card used for the transaction. Max length is 40 characters. - * - * Added in Saleor 3.23. - */ - brand?: InputMaybe; - /** - * Last characters of the gift card used for the transaction. Max length is 4 characters. - * - * Added in Saleor 3.23. - */ - lastChars?: InputMaybe; - /** - * Name of the payment method used for the transaction. Max length is 256 characters. - * - * Added in Saleor 3.23. - */ - name: Scalars['String']['input']; -}; - /** * Resend a gift card. * @@ -8764,8 +8953,6 @@ export type GiftCardSortField = | 'CURRENT_BALANCE' /** Sort gift cards by product. */ | 'PRODUCT' - /** Sort gift cards by rank. Note: This option is available only with the `search` filter. */ - | 'RANK' /** Sort gift cards by used by. */ | 'USED_BY'; @@ -8930,6 +9117,10 @@ export type GroupCountableEdge = { node: Group; }; +export type HttpMethod = + | 'GET' + | 'POST'; + /** Thumbnail formats for icon images. */ export type IconThumbnailFormatEnum = | 'ORIGINAL' @@ -12153,7 +12344,6 @@ export type Mutation = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. */ checkoutDeliveryMethodUpdate: Maybe; @@ -12221,7 +12411,6 @@ export type Mutation = { * * Triggers the following webhook events: * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout shipping method with the external one. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Optionally triggered when cached filtered shipping methods are invalid. * - CHECKOUT_UPDATED (async): A checkout was updated. * @deprecated Use `checkoutDeliveryMethodUpdate` instead. */ @@ -12365,15 +12554,33 @@ export type Mutation = { */ deleteWarehouse: Maybe; /** - * Calculates available delivery options for a checkout. + * Create new digital content. This mutation must be sent as a `multipart` request. More detailed specs of the upload format can be found here: https://github.com/jaydenseric/graphql-multipart-request-spec * - * Added in Saleor 3.23. + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContentCreate: Maybe; + /** + * Remove digital content assigned to given variant. * - * Triggers the following webhook events: - * - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered to fetch external shipping methods. - * - CHECKOUT_FILTER_SHIPPING_METHODS (sync): Triggered to filter shipping methods. + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. */ - deliveryOptionsCalculate: Maybe; + digitalContentDelete: Maybe; + /** + * Updates digital content. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContentUpdate: Maybe; + /** + * Generate new URL to digital content. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContentUrlCreate: Maybe; /** * Deletes draft orders. * @@ -12425,7 +12632,6 @@ export type Mutation = { * Triggers the following webhook events: * - NOTIFY_USER (async): A notification for the exported file. * - GIFT_CARD_EXPORT_COMPLETED (async): A notification for the exported file. - * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportGiftCards: Maybe; /** @@ -12436,7 +12642,6 @@ export type Mutation = { * Triggers the following webhook events: * - NOTIFY_USER (async): A notification for the exported file. * - PRODUCT_EXPORT_COMPLETED (async): A notification for the exported file. - * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportProducts: Maybe; /** @@ -12444,11 +12649,12 @@ export type Mutation = { * * Added in Saleor 3.18. * + * Note: this API is currently in Feature Preview and can be subject to changes at later point. + * * Requires one of the following permissions: MANAGE_DISCOUNTS. * * Triggers the following webhook events: * - VOUCHER_CODE_EXPORT_COMPLETED (async): A notification for the exported file. - * @deprecated Export functionality is deprecated and will be removed. All data can be fetched via the GraphQL API and parsed into the desired format by apps or external tools. */ exportVoucherCodes: Maybe; /** Prepare external authentication URL for user by custom plugin. */ @@ -14550,8 +14756,25 @@ export type MutationDeleteWarehouseArgs = { }; -export type MutationDeliveryOptionsCalculateArgs = { - id: Scalars['ID']['input']; +export type MutationDigitalContentCreateArgs = { + input: DigitalContentUploadInput; + variantId: Scalars['ID']['input']; +}; + + +export type MutationDigitalContentDeleteArgs = { + variantId: Scalars['ID']['input']; +}; + + +export type MutationDigitalContentUpdateArgs = { + input: DigitalContentInput; + variantId: Scalars['ID']['input']; +}; + + +export type MutationDigitalContentUrlCreateArgs = { + input: DigitalContentUrlCreateInput; }; @@ -15908,6 +16131,15 @@ export type NavigationType = /** Secondary storefront navigation. */ | 'SECONDARY'; +/** Represents the NEW_TAB target options for an app extension. */ +export type NewTabTargetOptions = { + /** + * HTTP method for New Tab target (GET or POST) + * @deprecated Use `settings` field directly. + */ + method: HttpMethod; +}; + /** An object with an ID */ export type Node = { /** The ID of the object. */ @@ -17550,6 +17782,7 @@ export type OrderLine = Node & ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_PRODUCTS, MANAGE_ORDERS. */ allocations: Maybe>; + digitalContentUrl: Maybe; /** * List of applied discounts * @@ -18714,8 +18947,6 @@ export type PageSortField = | 'PUBLICATION_DATE' /** Sort pages by publication date. */ | 'PUBLISHED_AT' - /** Sort pages by rank. Note: This option is available only with the `search` filter. */ - | 'RANK' /** Sort pages by slug. */ | 'SLUG' /** Sort pages by title. */ @@ -19055,7 +19286,7 @@ export type PageTypeUpdateInput = { addAttributes?: InputMaybe>; /** Name of the page type. */ name?: InputMaybe; - /** List of attribute IDs to be unassigned from the page type. */ + /** List of attribute IDs to be assigned to the page type. */ removeAttributes?: InputMaybe>; /** Page type slug. */ slug?: InputMaybe; @@ -19130,21 +19361,6 @@ export type PasswordChange = { user: Maybe; }; -/** - * Controls whether password-based authentication is allowed. - * - * ENABLED - any user can log in with a password. This is the default behavior. - * CUSTOMERS_ONLY - only customer users can log in with a password. - * If a staff user logs in with a password, they will be treated as a customer - * — the issued token will not contain any staff permissions. - * DISABLED - no user can log in with a password. - * - */ -export type PasswordLoginModeEnum = - | 'CUSTOMERS_ONLY' - | 'DISABLED' - | 'ENABLED'; - /** Represents a payment of a given type. */ export type Payment = Node & ObjectWithMetadata & { /** @@ -19201,6 +19417,11 @@ export type Payment = Node & ObjectWithMetadata & { modified: Scalars['DateTime']['output']; /** Order associated with a payment. */ order: Maybe; + /** + * Informs whether this is a partial payment. + * @deprecated This field is reserved for the Adyen Gateway plugin. For other gateways, its value is always `false`. This field will be removed in 3.23 along with the plugin. + */ + partial: Scalars['Boolean']['output']; /** Type of method used for payment. */ paymentMethodType: Scalars['String']['output']; /** List of private metadata items. Requires staff permissions to access. */ @@ -19608,19 +19829,13 @@ export type PaymentMethodDetailsFilterInput = { }; /** - * Details of the payment method used for the transaction. One of `card`, `other`, or `giftCard` is required. + * Details of the payment method used for the transaction. One of `card` or `other` is required. * * Added in Saleor 3.22. */ export type PaymentMethodDetailsInput = { /** Details of the card payment method used for the transaction. */ card?: InputMaybe; - /** - * Details of the gift card payment method used for the transaction. - * - * Added in Saleor 3.23. - */ - giftCard?: InputMaybe; /** Details of the non-card payment method used for this transaction. */ other?: InputMaybe; }; @@ -19766,13 +19981,11 @@ export type PaymentMethodTokenizationResult = * The following types are possible: * CARD - represents a card payment method. * OTHER - represents any payment method that is not a card payment. - * GIFT_CARD - represents a gift card payment method. * * */ export type PaymentMethodTypeEnum = | 'CARD' - | 'GIFT_CARD' | 'OTHER'; export type PaymentMethodTypeEnumFilterInput = { @@ -20621,6 +20834,7 @@ export type ProductBulkCreateErrorCode = | 'ATTRIBUTE_VARIANTS_DISABLED' | 'BLANK' | 'DUPLICATED_INPUT_ITEM' + | 'FILE_SIZE_LIMIT_EXCEEDED' | 'GRAPHQL_ERROR' | 'INVALID' | 'INVALID_PRICE' @@ -21042,6 +21256,7 @@ export type ProductErrorCode = | 'ATTRIBUTE_VARIANTS_DISABLED' | 'CANNOT_MANAGE_PRODUCT_WITHOUT_VARIANT' | 'DUPLICATED_INPUT_ITEM' + | 'FILE_SIZE_LIMIT_EXCEEDED' | 'GRAPHQL_ERROR' | 'INVALID' | 'INVALID_FILE_TYPE' @@ -21056,7 +21271,8 @@ export type ProductErrorCode = | 'REQUIRED' | 'UNIQUE' | 'UNSUPPORTED_MEDIA_PROVIDER' - | 'UNSUPPORTED_MIME_TYPE'; + | 'UNSUPPORTED_MIME_TYPE' + | 'VARIANT_NO_DIGITAL_CONTENT'; /** Event sent when product export is completed. */ export type ProductExportCompleted = Event & { @@ -21651,17 +21867,11 @@ export type ProductType = Node & ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_PRODUCTS. */ availableAttributes: Maybe; - /** - * Whether the product type has variants. - * @deprecated This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. - */ + /** Whether the product type has variants. */ hasVariants: Scalars['Boolean']['output']; /** The ID of the product type. */ id: Scalars['ID']['output']; - /** - * Whether the product type is digital - doesn't have any effect, it's present for backward-compatibility. - * @deprecated Will be removed in v3.24.0, use metadata or attributes instead. - */ + /** Whether the product type is digital. */ isDigital: Scalars['Boolean']['output']; /** Whether shipping is required for this product type. */ isShippingRequired: Scalars['Boolean']['output']; @@ -21837,11 +22047,6 @@ export type ProductTypeEnum = | 'SHIPPABLE'; export type ProductTypeFilterInput = { - /** - * - * - * DEPRECATED: this field will be removed. The field has no effect on the API behavior. This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. - */ configurable?: InputMaybe; ids?: InputMaybe>; kind?: InputMaybe; @@ -21852,13 +22057,9 @@ export type ProductTypeFilterInput = { }; export type ProductTypeInput = { - /** - * Determines if product of this type has multiple variants. This option mainly simplifies product management in the dashboard. There is always at least one variant created under the hood. - * - * DEPRECATED: this field will be removed. The field has no effect on the API behavior. This is a leftover from the past Simple/Configurable product distinction. Products can have multiple variants regardless of this setting. - */ + /** Determines if product of this type has multiple variants. This option mainly simplifies product management in the dashboard. There is always at least one variant created under the hood. */ hasVariants?: InputMaybe; - /** Determines if products are digital - doesn't have any effect, it's present for backward-compatibility. */ + /** Determines if products are digital. */ isDigital?: InputMaybe; /** Determines if shipping is required for products of this variant. */ isShippingRequired?: InputMaybe; @@ -21991,6 +22192,12 @@ export type ProductVariant = Node & ObjectWithAttributes & ObjectWithMetadata & channelListings: Maybe>; /** The date and time when the product variant was created. */ created: Scalars['DateTime']['output']; + /** + * Digital content for the product variant. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + */ + digitalContent: Maybe; /** External ID of this product. */ externalReference: Maybe; /** The ID of the product variant. */ @@ -22390,9 +22597,7 @@ export type ProductVariantChannelListing = Node & { /** The price of the variant. */ price: Maybe; /** - * Previous price of the variant in channel. Useful for providing promotion information required by customer protection laws such as EU Omnibus directive. - * - * Warning: This field is not updated automatically. Use Channel Listings mutation to update it manually. + * Prior price of the variant used for discount calculations. * * Added in Saleor 3.21. */ @@ -24017,6 +24222,20 @@ export type Query = { * Requires one of the following permissions: MANAGE_ORDERS, MANAGE_USERS. */ customers: Maybe; + /** + * Look up digital content by ID. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContent: Maybe; + /** + * List of digital content. + * + * Requires one of the following permissions: MANAGE_PRODUCTS. + * @deprecated Support for Digital Content is deprecated and will be removed in Saleor v3.23.0. This functionality is legacy and undocumented, and is not part of the supported API. Users should not rely on this behavior. + */ + digitalContents: Maybe; /** * List of draft orders. The query will not initiate any external requests, including filtering available shipping methods, or performing external tax calculations. * @@ -24344,7 +24563,7 @@ export type Query = { export type Query_EntitiesArgs = { - representations: Array; + representations?: InputMaybe>>; }; @@ -24492,6 +24711,19 @@ export type QueryCustomersArgs = { }; +export type QueryDigitalContentArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryDigitalContentsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + export type QueryDraftOrdersArgs = { after?: InputMaybe; before?: InputMaybe; @@ -24885,7 +25117,6 @@ export type QueryTransactionsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; - sortBy?: InputMaybe; where?: InputMaybe; }; @@ -25030,7 +25261,7 @@ export type RefundSettingsErrorCode = export type RefundSettingsUpdate = { errors: Array; /** Refund settings. */ - refundSettings: Maybe; + refundSettings: RefundSettings; /** @deprecated Use `errors` field instead. */ refundSettingsErrors: Array; }; @@ -25781,10 +26012,7 @@ export type ShippingMethod = Node & ObjectWithMetadata & { id: Scalars['ID']['output']; /** Maximum delivery days for this shipping method. */ maximumDeliveryDays: Maybe; - /** - * Maximum order price for this shipping method. - * @deprecated No longer supported - */ + /** Maximum order price for this shipping method. */ maximumOrderPrice: Maybe; /** * Maximum order weight for this shipping method. @@ -25805,10 +26033,7 @@ export type ShippingMethod = Node & ObjectWithMetadata & { metafields: Maybe; /** Minimum delivery days for this shipping method. */ minimumDeliveryDays: Maybe; - /** - * Minimal order price for this shipping method. - * @deprecated No longer supported - */ + /** Minimal order price for this shipping method. */ minimumOrderPrice: Maybe; /** * Minimum order weight for this shipping method. @@ -26579,6 +26804,12 @@ export type Shop = ObjectWithMetadata & { * Requires one of the following permissions: MANAGE_SETTINGS. */ allowLoginWithoutConfirmation: Maybe; + /** + * Enable automatic fulfillment for all digital products. + * + * Requires one of the following permissions: MANAGE_SETTINGS. + */ + automaticFulfillmentDigitalProducts: Maybe; /** List of available external authentications. */ availableExternalAuthentications: Array; /** List of available payment gateways. */ @@ -26612,6 +26843,18 @@ export type Shop = ObjectWithMetadata & { customerSetPasswordUrl: Maybe; /** Shop's default country. */ defaultCountry: Maybe; + /** + * Default number of max downloads per digital content URL. + * + * Requires one of the following permissions: MANAGE_SETTINGS. + */ + defaultDigitalMaxDownloads: Maybe; + /** + * Default number of days which digital content URL will be valid. + * + * Requires one of the following permissions: MANAGE_SETTINGS. + */ + defaultDigitalUrlValidDays: Maybe; /** * Default shop's email sender's address. * @@ -26681,12 +26924,6 @@ export type Shop = ObjectWithMetadata & { metafields: Maybe; /** Shop's name. */ name: Scalars['String']['output']; - /** - * Controls whether password-based authentication is allowed. - * - * Added in Saleor 3.23. - */ - passwordLoginMode: PasswordLoginModeEnum; /** List of available permissions. */ permissions: Array; /** List of possible phone prefixes. */ @@ -26733,12 +26970,6 @@ export type Shop = ObjectWithMetadata & { trackInventoryByDefault: Maybe; /** Returns translated shop fields for the given language code. */ translation: Maybe; - /** - * When enabled, stock availability is filtered by shipping zones and the destination address (legacy behavior). When disabled, stock availability is determined only by the direct warehouse-channel link, ignoring shipping zones. - * - * Added in Saleor 3.23. - */ - useLegacyShippingZoneStockAvailability: Scalars['Boolean']['output']; /** * Use legacy update webhook emission. When enabled, update webhooks (e.g. `customerUpdated`,`productVariantUpdated`) are sent even when only metadata changes. When disabled, update webhooks are not sent for metadata-only changes; only metadata-specific webhooks (e.g., `customerMetadataUpdated`, `productVariantMetadataUpdated`) are sent. * @@ -26846,7 +27077,6 @@ export type ShopErrorCode = | 'GRAPHQL_ERROR' | 'INVALID' | 'NOT_FOUND' - | 'PASSWORD_AUTH_RESTRICTION' | 'REQUIRED' | 'UNIQUE'; @@ -26880,6 +27110,8 @@ export type ShopMetadataUpdated = Event & { export type ShopSettingsInput = { /** Enable possibility to login without account confirmation. */ allowLoginWithoutConfirmation?: InputMaybe; + /** Enable automatic fulfillment for all digital products. */ + automaticFulfillmentDigitalProducts?: InputMaybe; /** * Charge taxes on shipping. * @@ -26888,6 +27120,10 @@ export type ShopSettingsInput = { chargeTaxesOnShipping?: InputMaybe; /** URL of a view where customers can set their password. */ customerSetPasswordUrl?: InputMaybe; + /** Default number of max downloads per digital content URL. */ + defaultDigitalMaxDownloads?: InputMaybe; + /** Default number of days which digital content URL will be valid. */ + defaultDigitalUrlValidDays?: InputMaybe; /** Default email sender's address. */ defaultMailSenderAddress?: InputMaybe; /** Default email sender's name. */ @@ -26924,12 +27160,6 @@ export type ShopSettingsInput = { * Warning: never store sensitive information, including financial data such as credit card details. */ metadata?: InputMaybe>; - /** - * Controls whether password-based authentication is allowed. - * - * Added in Saleor 3.23. - */ - passwordLoginMode?: InputMaybe; /** * When enabled, address fields that are not valid for a given country (according to Google's i18n address data) will be preserved instead of being removed during validation. Validation errors are still returned. * @@ -26948,12 +27178,6 @@ export type ShopSettingsInput = { reserveStockDurationAuthenticatedUser?: InputMaybe; /** This field is used as a default value for `ProductVariant.trackInventory`. */ trackInventoryByDefault?: InputMaybe; - /** - * When enabled, stock availability is filtered by shipping zones and the destination address (legacy behavior). When disabled, stock availability is determined only by the direct warehouse-channel link, ignoring shipping zones. - * - * Added in Saleor 3.23. - */ - useLegacyShippingZoneStockAvailability?: InputMaybe; /** * Use legacy update webhook emission. When enabled, update webhooks (e.g. `customerUpdated`,`productVariantUpdated`) are sent even when only metadata changes. When disabled, update webhooks are not sent for metadata-only changes; only metadata-specific webhooks (e.g., `customerMetadataUpdated`, `productVariantMetadataUpdated`) are sent. * @@ -28636,26 +28860,6 @@ export type TransactionEvent = Node & { type: Maybe; }; -/** - * Filter input for transaction events data. - * - * Added in Saleor 3.23. - */ -export type TransactionEventFilterInput = { - /** - * Filter transaction events by created at date. - * - * Added in Saleor 3.23. - */ - createdAt?: InputMaybe; - /** - * Filter transaction events by type. - * - * Added in Saleor 3.23. - */ - type?: InputMaybe; -}; - export type TransactionEventInput = { /** The message related to the event. */ message?: InputMaybe; @@ -28748,13 +28952,6 @@ export type TransactionEventTypeEnum = | 'REFUND_REVERSE' | 'REFUND_SUCCESS'; -export type TransactionEventTypeEnumFilterInput = { - /** The value equal to. */ - eq?: InputMaybe; - /** The value included in. */ - oneOf?: InputMaybe>; -}; - /** Filter input for transactions. */ export type TransactionFilterInput = { /** Filter by metadata fields of transactions. */ @@ -29104,27 +29301,6 @@ export type TransactionRequestRefundForGrantedRefundErrorCode = | 'REFUND_ALREADY_PROCESSED' | 'REFUND_IS_PENDING'; -export type TransactionSortField = - /** - * Sort transactions by creation date. - * - * Added in Saleor 3.23. - */ - | 'CREATED_AT' - /** - * Sort transactions by modification date. - * - * Added in Saleor 3.23. - */ - | 'MODIFIED_AT'; - -export type TransactionSortingInput = { - /** Specifies the direction in which to sort transactions. */ - direction: OrderDirection; - /** Sort transactions by the selected field. */ - field: TransactionSortField; -}; - /** * Update transaction. * @@ -29198,25 +29374,7 @@ export type TransactionWhereInput = { OR?: InputMaybe>; /** Filter by app identifier. */ appIdentifier?: InputMaybe; - /** - * Filter transactions by created at date. - * - * Added in Saleor 3.23. - */ - createdAt?: InputMaybe; - /** - * Filter by transaction events. Each list item represents conditions that must be satisfied by a single event. The filter matches transactions that have related events meeting all specified groups of conditions. - * - * Added in Saleor 3.23. - */ - events?: InputMaybe>; ids?: InputMaybe>; - /** - * Filter transactions by modified at date. - * - * Added in Saleor 3.23. - */ - modifiedAt?: InputMaybe; /** Filter by PSP reference. */ pspReference?: InputMaybe; }; @@ -29690,9 +29848,7 @@ export type UserSortField = /** Sort users by last name. */ | 'LAST_NAME' /** Sort users by order count. */ - | 'ORDER_COUNT' - /** Sort users by rank. Note: This option is available only with the `search` filter. */ - | 'RANK'; + | 'ORDER_COUNT'; export type UserSortingInput = { /** Specifies the direction in which to sort users. */ @@ -31907,6 +32063,15 @@ export type WeightUnitsEnum = | 'OZ' | 'TONNE'; +/** Represents the WIDGET target options for an app extension. */ +export type WidgetTargetOptions = { + /** + * HTTP method for Widget target (GET or POST) + * @deprecated Use `settings` field directly. + */ + method: HttpMethod; +}; + /** _Entity union as defined by Federation spec. */ export type _Entity = Address | App | Category | Collection | Group | Order | PageType | Product | ProductMedia | ProductType | ProductVariant | User; diff --git a/packages/domain/src/objects/Marketplace.ts b/packages/domain/src/objects/Marketplace.ts new file mode 100644 index 00000000..7062620c --- /dev/null +++ b/packages/domain/src/objects/Marketplace.ts @@ -0,0 +1,10 @@ +export interface VendorProfile { + /** + * ID of the vendor. + */ + id: string; + /** + * Display name of the vendor. + */ + name: string; +} diff --git a/packages/features/src/cart/shared/components/cart-details.tsx b/packages/features/src/cart/shared/components/cart-details.tsx index 0a370bad..1a58fe7d 100644 --- a/packages/features/src/cart/shared/components/cart-details.tsx +++ b/packages/features/src/cart/shared/components/cart-details.tsx @@ -7,6 +7,8 @@ import { useEffect, useRef, useState } from "react"; import { type Cart } from "@nimara/domain/objects/Cart"; import type { AsyncResult } from "@nimara/domain/objects/Result"; import { type User } from "@nimara/domain/objects/User"; +import { Line } from "@nimara/features/shared/shopping-bag/components/line"; +import { groupLinesByVendorId } from "@nimara/features/shared/shopping-bag/helpers"; import { ShoppingBag } from "@nimara/features/shared/shopping-bag/shopping-bag"; import { LocalizedLink, useRouter } from "@nimara/i18n/routing"; import { Button } from "@nimara/ui/components/button"; @@ -15,6 +17,7 @@ import { cn } from "@nimara/ui/lib/utils"; export interface CartDetailsProps { cart: Cart; + isMarketplaceEnabled?: boolean; lineCheckoutIdMap?: Record; onCartUpdate: (cartId: string) => Promise; onLineDelete: (params: { @@ -31,11 +34,14 @@ export interface CartDetailsProps { checkoutSignIn: string; }; user: User | null; + vendorIdNames?: Record; } export const CartDetails = ({ cart, + isMarketplaceEnabled, user, + vendorIdNames, lineCheckoutIdMap, onLineQuantityChange, onLineDelete, @@ -136,12 +142,17 @@ export const CartDetails = ({ + +
+ diff --git a/packages/features/src/cart/shared/components/cart-shell.tsx b/packages/features/src/cart/shared/components/cart-shell.tsx new file mode 100644 index 00000000..3be449cb --- /dev/null +++ b/packages/features/src/cart/shared/components/cart-shell.tsx @@ -0,0 +1,9 @@ +export const CartShell = ({ children }: { children: React.ReactNode }) => { + return ( +
+
+ {children} +
+
+ ); +}; diff --git a/packages/features/src/cart/shop-basic-cart/standard.tsx b/packages/features/src/cart/shop-basic-cart/standard.tsx index 9c1e25b8..3907c926 100644 --- a/packages/features/src/cart/shop-basic-cart/standard.tsx +++ b/packages/features/src/cart/shop-basic-cart/standard.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { getTranslations } from "next-intl/server"; import { Suspense } from "react"; +import { CartShell } from "@nimara/features/cart/shared/components/cart-shell"; import { ShoppingBagSkeleton } from "@nimara/features/shared/shopping-bag/shopping-bag-skeleton"; import { CartDetails } from "../shared/components/cart-details"; @@ -28,33 +29,31 @@ export const StandardCartView = async (props: CartViewProps) => { } = props; return ( -
-
- }> - } - render={({ cart, user }) => ( - - )} - /> - -
-
+ + }> + } + render={({ cart, user }) => ( + + )} + /> + + ); }; diff --git a/packages/features/src/product-detail-page/shop-basic-pdp/standard.tsx b/packages/features/src/product-detail-page/shop-basic-pdp/standard.tsx index f4021808..e235dd10 100644 --- a/packages/features/src/product-detail-page/shop-basic-pdp/standard.tsx +++ b/packages/features/src/product-detail-page/shop-basic-pdp/standard.tsx @@ -71,6 +71,7 @@ export const StandardPDPView = async ({ checkoutId={checkoutId} cartPath={paths.cart} region={region} + isMarketplaceEnabled={marketplaceEnabled} addToBagAction={addToBagAction} /> diff --git a/packages/features/src/shared/shopping-bag/components/line.tsx b/packages/features/src/shared/shopping-bag/components/line.tsx index a1a20918..ceeeff91 100644 --- a/packages/features/src/shared/shopping-bag/components/line.tsx +++ b/packages/features/src/shared/shopping-bag/components/line.tsx @@ -132,8 +132,8 @@ export const Line = ({ }, [width]); return ( -
-
+
+
{thumbnail ? ( )}
-
+

-
+
{isLineEditable ? ( <>