type Query { _empty: String """ Page through registered customers (excluding guest / temporary rows). Used by the admin's customer list, by export jobs, and by CRM integrations. For a single customer by id, use customer; by email, use customerByEmail. """ customers( """Maximum number of customers to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Customer] """ Fetch a single customer by id. Returns null when the id doesn't resolve. Includes the activity-stats sidecar and the customer's addresses in the same response. """ customer( """Customer id to fetch.""" id: ID! ): Customer """ Fetch a customer by email. Email lookup is restricted to registered (non-temporary) customers; the resolver excludes temporary_customer rows, so guest checkouts sharing an email with a registered account won't be returned. """ customerByEmail( """ Email address to look up. Case-insensitive in practice — emails are normalized to lowercase at registration. """ emailAddress: String! ): Customer """ Fetch the activity-stats side car for one customer: login counters, last-login timestamps, failed-login tally, account creation / modification dates, and marketing-source attribution. Used by the admin's customer detail page to render 'Last login' and 'Failed attempts' badges, by security alerting on unusually high failed-login counts, and by lifecycle marketing flows. For the customer's profile, addresses, and group memberships see the customer query. """ customerInfo( """Customer to fetch the activity-stats side car for.""" customerId: ID! ): CustomerInfo """ Page through the catalog's products. Returns the headline fields only (id, name from model, image, price) — there's no language selector on this query, so descriptions / material aren't included. Used for catalog feeds, sitemaps, and admin / ERP exports that need a product enumeration without per-language hydration. For the full product detail (with descriptions, in a specific language) use product. For category-scoped product fetches join via productsToCategories. """ products( """Maximum number of products to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Product] """ Fetch a single product by id, with its localized description rows. Pass languageId to scope the description / shortDescription / material to one language; omit it to receive the row with the lowest language_id (the resolver orders ASC and takes the first). Used to render the storefront's product detail page header and the admin product editor's overview tab. For configurable options see productAttributes; for variant-level stock and pricing see productStock; for the variant-aware image gallery see productExtraImages; for PDFs / CAD / videos see productsMedia. """ product( """Product id to fetch.""" id: ID! """ Language to scope description rows to. Omit to get the row with the lowest language_id. """ languageId: Int ): Product """ List the extra (non-main) images attached to one product, in display order, with localized captions. Use this on product detail pages alongside the main Product.image, and for variant-aware galleries (filter the result by optionId == selected variant's option-value id, plus optionId == 0 for variant-agnostic images). For other media types — PDFs, datasheets, videos, CAD — use productsMedia instead. """ productExtraImages( """Product to fetch extra images for.""" productId: ID! """ Language id for the localized caption (1=EN, 5=SV, 6=FI, 9=ES, 14=IT — tenants may extend; resolve dynamically via the languages query). """ languageId: Int ): [ProductExtraImage] """ List the media attachments for one product (PDFs, CAD files, lighting profiles, etc.), in display order, with localized descriptions. Used to render the storefront's "Documents & downloads" block on product detail pages, and by the admin product editor's media tab. Pass includeCategory=true (the default) to resolve each row's ProductMediaCategory in the same round-trip; set false when you only need the file list and want to skip the category JOIN. For images that go in the main product gallery (with variant- specific filtering), use productExtraImages instead. """ productsMedia( """Product to fetch media for.""" productId: ID! """ Language id for the localized name + description (1=EN, 5=SV, 6=FI, 9=ES, 14=IT — tenants may extend; resolve dynamically via the languages query). """ languageId: Int """ Whether to resolve each media row's category in the same response. Defaults to true. """ includeCategory: Boolean = true ): [ProductMedia] """ List the countries the tenant supports. Used everywhere an address is captured: customer registration, checkout, address book, admin order builder. The storefront also picks the shopper's default currency from Country.defaultCurrencyId on first visit, and renders printed addresses with the AddressFormat referenced by Country.addressFormatId. For the address-rendering templates themselves see addressFormats; for the currency definitions see currencies. """ countries( """Maximum number of countries to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Country] """ List every configuration group, in display order. Returns just the group metadata — for a flat list of every setting (or per-group) use configurations; for groups eagerly hydrated with their settings in one round-trip, use configurationGroupsWithSettings. """ configurationGroups: [ConfigurationGroup] """ Page through configuration settings. Pass groupId to scope to one section of the admin UI (and the resolved group field will be hydrated in the same response); omit it to enumerate every setting across the platform. Used by the admin's configuration editor, by tooling that audits platform settings, and by deploy / migration scripts that diff settings across tenants. For a tree-shaped fetch (groups + nested configurations) see configurationGroupsWithSettings. """ configurations( """ Restrict to settings inside one ConfigurationGroup. Omit to span all groups. """ groupId: Int """Maximum number of settings to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Configuration] """ List every configuration group with its settings hydrated in one response. The resolver runs a single LEFT JOIN across configuration_group + configuration, ordered by group sort order, and folds the rows into a tree shape — groups with their nested configurations[] field populated. Use this when the admin UI needs the full settings tree for a rendering pass (e.g. when the configuration editor first loads). For a flat list of just the groups, or settings filtered by group, use configurationGroups / configurations — those are lighter when you don't need every setting in every group. """ configurationGroupsWithSettings: [ConfigurationGroup] """ List admin-facing error notifications in insertion order. Used by the admin dashboard's notification inbox to render the operator alert queue, by the email-dispatch worker that emails un-emailed entries, and by oncall runbooks that need to inspect what's been raised recently. No filter args today — paginate with limit / offset to walk the full queue. For per-action audit entries (logins, exports, password changes) see actionRecorderEntries instead; this surface is for error-class operator alerts, not action audits. """ adminNotifications( """Maximum number of notifications to return.""" limit: Int """Offset for pagination.""" offset: Int ): [AdminNotification] """ List product-creation / lifecycle audit entries, newest first. Pass productId to scope to one product's history (the admin product editor's 'history' tab uses this), userId to scope to one admin's activity (auditing what one operator has touched), or omit both to walk the full feed. Used by the admin product editor's history view, by accountability reports surfacing 'who created what' across the catalog, and by ERP reconciliation flows. For per-price audit see productPriceHistory; for the broader cross-module audit log see actionRecorderEntries. """ adminTrackingCreate( """Restrict to one product's history.""" productId: Int """Restrict to one admin's activity.""" userId: Int """Maximum number of entries to return.""" limit: Int """Offset for pagination.""" offset: Int ): [AdminTrackingCreate] """ List admin user accounts for the tenant. Used by the admin user-management page (visible to superadmins), by access reviews that audit who has the right to do what, and by the SSO bridge to map an external identity to a local admin record. No filter args today — paginate with limit / offset to walk the full list. For the audit trail of what each admin has done, see actionRecorderEntries. """ administrators( """Maximum number of administrators to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Administrator] """ List the currencies enabled for the tenant. Used by the storefront's currency switcher to render the available options, by the price formatter to look up the active currency's symbols and rate, and by the admin panel's currency settings tab. For the country-level default-currency mapping (which currency a shopper from a given country sees by default) check Country.defaultCurrencyId on the countries query — this surface enumerates the currencies themselves. """ currencies( """Maximum number of currencies to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Currency] """ Page through orders, newest first. Pass customerId to scope to one customer's order history (used by the customer account 'My orders' page and admin customer detail). Used by the admin order list, by fulfilment dashboards, by ERP / accounting exports, and by reports that walk the full order book. For one order's full detail (line items, totals, status history, payment notifications) use order with the appropriate sub-fields. """ orders( """Maximum number of orders to return.""" limit: Int """Offset for pagination.""" offset: Int """Filter to one customer's orders.""" customerId: Int ): [Order] """ Fetch a single order by id, with the full record hydrated. Returns null when the id doesn't resolve. Sub-fields (items, totals, discountCoupons, vivawalletPayments, etc.) lazy-load on demand, so a minimal selection still does a fast single-row read. """ order( """Order id to fetch.""" id: ID! ): Order """ Page through categories across the full tree. Returns each category with all of its CategoryDescription rows attached, so a single fetch can render any language. Used by sitemap builders, by exports / ERP sync, and by the admin's category browser. For a single category by id, use category instead — it lets you scope to one language to skip extraneous CategoryDescription rows. """ categories( """Maximum number of categories to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Category] """ Fetch a single category by id. Pass languageId to receive only that language's CategoryDescription row in the descriptions array (the storefront uses this on category page render); omit it to receive every language for the category (the admin category editor uses this). """ category( """Category id to fetch.""" id: ID! """Language to scope CategoryDescription rows to. Omit for all languages.""" languageId: Int ): Category """ Return every order status defined for the connected tenant in the requested language. Use this to render status filters and badges in the admin panel, and to translate Order.status / OrderStatusHistory.statusId numeric ids to display labels. Tenants extend the canonical id set with custom workflow stages and payment-provider intermediate states; resolve dynamically rather than hardcoding. For per-order transition history use orderStatusHistory; to record a new status transition use addOrdersStatusHistory. """ orderStatuses( """ Language id to fetch labels in. Resolve via the languages query rather than hardcoding — tenants may extend the platform default set (1=EN, 5=SV, 6=FI, 9=ES, 14=IT). """ languageId: Int! ): [OrderStatus] """ Paginated status-transition timeline for one order, returned newest-first. Use this to render the "status history" widget on an admin order detail view, or to investigate when an order entered a particular state. Pass statusId to filter to one specific status (e.g. only the Shipped transitions). For the broader change-log including non-status edits see ordersHistory; to record a new transition see the addOrdersStatusHistory mutation. """ orderStatusHistory( """Order to fetch the timeline for.""" orderId: Int! """Filter to transitions to this single status id.""" statusId: Int """Maximum rows to return.""" limit: Int """Number of rows to skip for pagination.""" offset: Int ): [OrderStatusHistory] """ Paginated audit log of changes for one order, returned newest-first. Use this for the "history" tab on an order detail view, for compliance audits, or to reconstruct who changed what and when on an order that's now in dispute. Filter by userId to see only one administrator's edits, or by date range (startDate / endDate, ISO YYYY-MM-DD) to scope to one investigation window. For status-only transitions there's a more focused view via orderStatusHistory. """ ordersHistory( """Order to fetch the change log for.""" orderId: Int! """Filter to changes made by this administrator user id.""" userId: Int """ISO date (YYYY-MM-DD) — return only entries on or after this date.""" startDate: String """ISO date (YYYY-MM-DD) — return only entries on or before this date.""" endDate: String """Maximum rows to return (default 10).""" limit: Int """Number of rows to skip for pagination (default 0).""" offset: Int ): [OrderHistoryEntry!]! """ List the languages enabled for the tenant. Used by the storefront's language switcher, by the admin's language picker, and by any integration that needs the tenant's full set of canonical language ids before resolving per-language description rows on other types. Returns the platform's shared core ids plus any tenant-defined extensions. The image / directory fields come back nested under metadata to keep the i18n-bundle plumbing separate from the user-facing identity (id / code / name). """ languages( """Maximum number of languages to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Language] """ List availability labels, optionally filtered by language. Pass languageId to get only the labels for one storefront language; omit it to receive every (id, languageId) pair the tenant has defined. """ availabilities(languageId: Int): [Availability] """ Page through the warehouses the tenant has configured. Used by the storefront's 'Find a store' / store-locator picker, by the admin's warehouse list, by the order-creation flow when an operator picks where to ship from, and by ERP / WMS reconciliation reports. For per-warehouse on-hand quantities, follow Warehouse.stock or use the warehouseStock query directly. For the variant-level stock totals (across all warehouses), see productStock. """ warehouses( """Maximum number of warehouses to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Warehouse] """ List per-warehouse on-hand quantities. All filters are optional: pass productId to get a product's split across all warehouses, pass warehouseId to list every product's quantity at one warehouse, or pass both to get the single (warehouse, product) row. Used by the storefront's 'check stock at your local store' UI, by the admin's warehouse inventory reports, and by fulfilment routing logic. For variant-level totals across warehouses see productStock; for the full warehouse master list (locations, codes, defaults) see the warehouses query. """ warehouseStock( """Restrict to one warehouse. Omit to span all warehouses.""" warehouseId: Int """Restrict to one product. Omit to span all products.""" productId: Int ): [WarehouseStock] """ Page through the brands in the catalog. Returns each manufacturer with all its ManufacturerInfo rows attached, so a single fetch can render any language. Used by the storefront's brand index, by sitemap builders, and by ERP exports. For one brand by id (with the option to scope description rows to one language), use manufacturer. """ manufacturers( """Maximum number of brands to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Manufacturer] """ Fetch a single brand by id. Pass languageId to receive only that language's ManufacturerInfo row in the infos array (the storefront uses this on brand-page render); omit it to receive every language (the admin brand editor uses this). """ manufacturer( """Brand id to fetch.""" id: ID! """Language to scope ManufacturerInfo rows to. Omit for all languages.""" languageId: Int ): Manufacturer """ Fetch a single quote by id, with line items hydrated. Returns null when the id doesn't resolve. Used by the admin quote editor's detail view, by the customer-facing 'My quote' page, and by the quote-to-order conversion flow when an operator accepts a quote. """ quote( """Quote id to fetch.""" id: ID! ): Quote """ List quotes for one customer, optionally scoped to one statusId. Used by the customer account 'My quotes' page (typically scoped to 'Open' + 'Proposal sent') and by the admin's per-customer quote list. """ quotesByCustomer( """Customer id to fetch quotes for.""" customerId: Int! """Filter to quotes with this status id.""" statusId: Int """Maximum number of quotes to return.""" limit: Int ): [Quote!]! """ List quotes for one anonymous session — quotes captured before the customer logged in or registered. Used to migrate anonymous quotes onto a fresh customer account at sign-up time. """ quotesBySession( """Session id to fetch quotes for.""" sessionId: String! """Filter to quotes with this status id.""" statusId: Int """Maximum number of quotes to return.""" limit: Int ): [Quote!]! """ Return the quote-status definitions in the requested language. Used to render status filters and badges in the admin quote list, and to translate Quote.statusId / QuoteStatusEntry.statusId numeric ids to display labels. Tenants may extend the canonical id set with custom workflow stages; resolve dynamically rather than hardcoding. """ quoteStatuses( """Language to fetch labels in. Required.""" languageId: Int! ): [QuoteStatus] """ Return the status-transition timeline for one quote. Mirrors orderStatusHistory but for quotes. Used by the admin quote detail's history tab; to record a new transition (and optionally notify the customer) use addQuoteStatusHistory. """ quoteStatusHistory( """Quote to fetch the timeline for.""" quoteId: ID! ): [QuoteStatusEntry] """ List the attachments belonging to one quote, in id order. Used by the admin quote editor's 'Files' tab, by the customer-facing quote view that shows the proposal PDF alongside any uploaded briefs, and by the quote-to-order conversion flow when the seller wants to forward attachments through to the resulting order. """ quoteFiles( """Quote to fetch attachments for.""" quoteId: ID! ): [QuoteFile] """ List the quote groups defined for the tenant in the requested language. Used to render the group dropdown on the storefront's quote-request form, and the same selector in the admin quote editor when an operator creates a quote on a customer's behalf. Resolves only to rows matching the languageId — the resolver doesn't fall back to other languages, so ensure the language has been seeded with the full group set or pass a known language id. """ quoteGroups( """Language to fetch group labels in. Required.""" languageId: Int! ): [QuoteGroup!]! """ List the configurable options for one product, with each option's selectable values, in the requested language. Use this to render the variant selector on product detail pages, to populate option pickers in the admin order builder, and to validate incoming OrderItemVariant choices on createOrder. For per-product catalog attributes (size charts, technical specs, EAV-style fields), see the products query — those live on the eav_attribute system, distinct from this product-options system. """ productAttributes( """Product to fetch options for.""" productId: ID! """ Language id for localized name + description (1=EN, 5=SV, 6=FI, 9=ES, 14=IT — tenants may extend; resolve dynamically via the languages query). """ languageId: Int ): [ProductOption] """ List the stock rows for one product. Pass variant to scope to a single SKU (matching ProductStock.variant); omit it to receive every variant of the product. Use this for product detail pages that need per-variant availability, for the admin product editor's stock tab, and for ERP reconciliation flows. For per-warehouse split see warehouseStock; for the price-change audit trail see productPriceHistory. """ productStock( """Product to fetch stock rows for.""" productId: ID! """ Variant identifier to filter to a single SKU. Omit for all variants of the product. """ variant: String ): [ProductStock] """ List historical price changes for a product, newest first. Pass stockId to scope to one variant, omit it to receive every variant's history plus the product-level changes (stockId = 0). Filter by startDate / endDate to bracket a time window, by userName to audit one admin's pricing edits, and use limit / offset to page through long histories. Used by the admin pricing tab's history view, by the reporting dashboard that surfaces frequent price changers, and by ERP reconciliation flows that need to verify an external price update landed. For the current live price use ProductStock.price (per-variant) or the price field on Product (product-level). """ productPriceHistory( """Product to fetch the price history for.""" productId: ID! """ Variant (products_stock) id to scope the history to a single SKU. Pass 0 for product-level changes only. Omit for everything. """ stockId: ID """Lower bound of the date filter (inclusive). ISO-8601 date or datetime.""" startDate: String """Upper bound of the date filter (inclusive). ISO-8601 date or datetime.""" endDate: String """Filter to changes made by this admin username.""" userName: String """Maximum number of history rows to return.""" limit: Int """Offset for pagination.""" offset: Int ): [ProductPriceHistory] """ List product-to-category link rows. All filters are optional and composable: pass productId to get every category one product belongs to, pass categoryId to get every product in one category, or pass both to check for the existence of a single link. Use limit / offset to page through large categories (the storefront's category listing pages do). For full Product or Category records (with their fields, prices, descriptions), follow the resolved product / category fields, or query products / categories directly — productsToCategories is a navigation / membership surface, not a full product or category fetch. """ productsToCategories( """Restrict to links for this product. Omit to span all products.""" productId: ID """Restrict to links for this category. Omit to span all categories.""" categoryId: ID """Maximum number of links to return.""" limit: Int """Offset for pagination.""" offset: Int ): [ProductCategoryLink] """ List the saved-basket lines for one customer, newest line first. Pass cartId to scope to one named cart (lets a single customer hold multiple parallel baskets — saved-for-later vs. active checkout vs. a B2B quote draft). Used by the storefront to rehydrate the cart on login, by the abandoned-basket email job, by the admin order-builder when an operator wants to create an order on behalf of a logged-in customer, and by B2B portal flows that quote off saved baskets. For finalized order lines see ordersProducts; for shared-basket flows see sharedBasket. """ customersBasket( """Customer to fetch the saved basket for.""" customersId: ID! """ Cart key, when the customer holds multiple parallel baskets. Omit to receive every basket the customer has. """ cartId: String ): [CustomersBasketItem] """ Page through every coupon definition in the tenant. Each coupon is returned with all its applicability arrays (categories / products / customers / orders / manufacturers / shippingMethods / zones) and the redemption counters (maxUse vs numberAvailable). Used by the admin's coupon list / editor, by the storefront self-service 'enter code' flow that pre-validates a code on the fly, and by reconciliation reports. For per-order applied-coupons see discountCouponsToOrders / Order.discountCoupons; for per-customer attach / revoke see the customer-coupon mutations. """ discountCoupons( """Maximum number of coupons to return.""" limit: Int """Offset for pagination.""" offset: Int ): [DiscountCoupon] """ Fetch one coupon by id, with applicability arrays and counters fully hydrated. Returns null when the id doesn't resolve. Used by the storefront's 'enter code' flow to validate a single code and look up its discount value before applying it to the basket. """ discountCoupon( """Coupon id to fetch.""" id: ID! ): DiscountCoupon """ Page through coupon-to-order links. Filters compose with AND: pass orderId to fetch every coupon redeemed against one order (typically reached via Order.discountCoupons instead), or couponCode to walk the redemption history of one coupon (also reachable via DiscountCoupon.orders). Used by coupon-performance reports (top-redeemed coupons, redemption-by-week dashboards), by refund flows that need to audit which coupons applied to a returned order, and by ERP / accounting exports. """ discountCouponsToOrders( """Restrict to one order's redemption links.""" orderId: ID """Restrict to one coupon's redemption history.""" couponCode: ID """Maximum number of link rows to return.""" limit: Int """Offset for pagination.""" offset: Int ): [DiscountCouponOrderLink] """ Return every line on a single order, including each line's variants. Use this when rendering an order detail view where you need the line breakdown separately from the order header. The same data is exposed through Order.items, which is preferable when you're already fetching the order and want to avoid a round trip. To modify lines on an existing order, see the addOrdersProduct / updateOrdersProduct / removeOrdersProduct mutations, which write to the same underlying orders_products row. """ orderItems(orderId: ID!): [OrderItem!]! """ Return the totals breakdown for one order — the rows that render as the "Subtotal / Tax / Shipping / Discount / Grand total" block on an invoice or order summary. Pass class to scope to one row category (e.g. just 'Tax' to render the tax breakdown). Omit it to receive every row sorted by sortOrder so the result can be rendered top-to-bottom directly. Order.totals exposes the same data with the same arguments and is preferable when you're already fetching the order header. """ orderTotals(orderId: ID!, class: String): [OrderTotal!]! """ List service pages in the requested language. Pass categoryId to scope to one CMS section (e.g. the 'Help' branch); pass displayFooter = true to restrict to footer-visible pages only. Used by the storefront's CMS routing layer to render individual information pages, by the footer layout to enumerate footer links, and by sitemap builders. For the navigation tree of categories see serviceCategories; for product-catalog content see products / categories. """ services( """Language to fetch description rows in. Required.""" languageId: Int! """Restrict to pages under one ServiceCategory id.""" categoryId: Int """ When true, restrict to pages with displayFooter = 1 (footer-visible only). """ displayFooter: Boolean ): [Service] """ List service categories in the requested language, optionally scoped to one branch of the tree (parentId) or to footer-visible categories only (displayFooter). Returns the categories with their children nested in-place, so a single fetch can render the full storefront CMS navigation tree. Used by the storefront's CMS-page navigation, by the footer layout, and by the admin's service-category editor. For the individual service pages living under each category, see the services query. """ serviceCategories( """Language to fetch description rows in. Required.""" languageId: Int! """ Restrict to children of one parent category id. Pass 0 to scope to top-level categories; omit to span all. """ parentId: Int """ When true, restrict to categories with displayFooter = 1 (footer-visible only). """ displayFooter: Boolean ): [ServiceCategory] """ Page through reviews. Filters compose with AND: pass productId to scope to one product (powering the product-page review widget), statusId to filter by moderation state (e.g. 'approved' for storefront, 'pending' for the admin moderation queue), languageId to scope ReviewDetail rows to one language. Used by the storefront's product-page review listing, by the admin moderation tools, and by review-syndication exports. For aggregate counts and average ratings (without the full review bodies) see reviewSummaries. """ reviews( """Restrict to reviews for one product.""" productId: ID """Restrict to reviews with this moderation status id.""" statusId: Int """Scope ReviewDetail rows to one language id.""" languageId: Int """Maximum number of reviews to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Review] """ Aggregate review counts and ratings across one or many products. Pass productId to fetch the summary for a single product (powering the rating badge on a product page); omit it to walk every product's aggregate (used by sitemap / feed builders that emit aggregate rating microdata). """ reviewSummaries( """Restrict to one product's summary.""" productId: ID """Maximum number of summaries to return.""" limit: Int """Offset for pagination.""" offset: Int ): [ReviewSummary] """ List every banner the tenant has defined, in id order. Use limit / offset to page through. For zone-scoped fetches (e.g. only the banners in the homepage hero zone) use bannersByGroup instead — it's the same surface filtered by Banner.group. For per-day click / impression series, use the Banner.historyByDate field on each banner, or the bannerHistory query directly. """ banners( """Maximum number of banners to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Banner] """ List the banners belonging to one storefront zone. The group code is free-form and tenant-defined — common examples are 'home', 'sidebar', 'category-N'. Used by the storefront's banner renderer to populate one zone at a time. """ bannersByGroup( """ Group code identifying the zone. Tenant-defined, see Banner.group for examples. """ group: String! """Maximum number of banners to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Banner] """ List FAQ categories with their nested questions. Pass languageId to scope description rows to one language (skipping non-matching FaqCategoryDescription / FaqQuestionDescription rows in the response). visibleOnly = true filters out hidden categories AND questions; includeHiddenQuestions overrides the visibility filter for questions only (categories still respect visibleOnly). sortByOrder = true sorts categories and questions by their sortOrder fields. Used to render the storefront's help-center landing page, the accordion-style FAQ sections on product / category pages, and to seed the AI assistant's grounding corpus. For a single question by id or slug, use faqQuestion. """ faqCategories( """Language to scope description rows to. Omit for all languages.""" languageId: Int """ When true, drop hidden categories and (unless includeHiddenQuestions overrides) hidden questions. """ visibleOnly: Boolean """ When true, include hidden questions inside visible categories — overrides the question-side filter applied by visibleOnly. """ includeHiddenQuestions: Boolean """ When true, sort categories and questions by their sortOrder fields. When false, fall back to id ordering. """ sortByOrder: Boolean ): [FaqCategory] """ Fetch a single FAQ question by id or by slug. Pass id to look up the question directly; pass slug to look it up by the per-language FaqQuestionDescription.url field (the routable form when enableUrl = 1). Use languageId to scope returned descriptions to one language; visibleOnly = true returns null when the question is hidden. """ faqQuestion( """Question id to fetch.""" id: ID """ Slug to look up. Resolves against FaqQuestionDescription.url; pair with languageId for unambiguous matching. """ slug: String """Language to scope the slug match and the returned descriptions.""" languageId: Int """When true, return null for questions whose visibility = 0.""" visibleOnly: Boolean ): FaqQuestion """ List customer groups available to the requesting context, with each group's dynamicDiscounts and priceList eagerly hydrated. The resolver also reads the X-Customer-Type header and filters to groups whose customerType is 'all' or matches the header — so a B2C front-end and a B2B portal hitting the same endpoint see different group sets without explicit filter args. Pass type to scope to a specific groupsType (e.g. 'dynamic' to fetch only the matrix groups), and activeOnly = true to drop groups outside their dateStart / dateEnd window. Used by the storefront's pricing engine, by the admin's group editor, and by ERP exports. For per-rule cross-group fetch see customerGroupPriceList. """ customerGroups( """ Restrict to groups of this groupsType (e.g. 'fixed', 'dynamic', 'matrix', 'list'). """ type: String """ When true, only return groups currently within their dateStart / dateEnd window (and only active dynamic-discount + price-list rows). Defaults to false. """ activeOnly: Boolean ): [CustomerGroup] """ Page through the change log for one customer, newest first. Filters compose with AND: sourceTable scopes to one logical table (e.g. 'address_book' to see only address edits), userId scopes to one admin's edits to the customer. Used by the admin's customer detail history tab, by GDPR / data-subject-access requests that need a complete edit trail, and by abuse / fraud investigation. For the cross-module action audit see actionRecorderEntries; for product-record edits see adminTrackingCreate. """ customersHistory( """Customer to fetch history for.""" customerId: Int! """Filter to changes made by one admin id.""" userId: Int """ Filter to changes on one source table (e.g. 'customers', 'address_book', 'customers_info'). """ sourceTable: String """Maximum number of entries to return.""" limit: Int """Offset for pagination.""" offset: Int ): [CustomerHistoryEntry!]! """ List ERP-mirrored customer records, with optional filters. Used by the admin's ERP-status panel to inspect what the platform has cached from the source ERP, by B2B portals that need contract-side fields (credit limit, payment term, sales rep) at order placement, and by reconciliation reports comparing the ERP cache with the platform-side Customer record. Filter args use the Finnish ERP field names: asiakaskoodi (customer code), email, alennusryhma (discount group). For the platform-side customer record (logins, addresses, group membership), join on Customer.apiId = CustomersApiCustomer.asiakaskoodi via the customer query. """ customersApi( """Filter to one ERP customer code (Finnish 'asiakaskoodi').""" asiakaskoodi: String """ Filter to one email address (case-sensitive — matches the ERP-side normalization). """ email: String """Filter to one ERP discount-group code (Finnish 'alennusryhma').""" alennusryhma: String """Maximum number of records to return.""" limit: Int """Offset for pagination.""" offset: Int ): [CustomersApiCustomer!]! """ Page through group-based price-list rules across all customer groups, with optional filters. This is the cross-group surface — equivalent in shape to CustomerGroup.priceList but reachable without first loading a parent group, useful when the caller has a productId in hand and wants every group's override for it, or wants to validate price rules around a specific date. Pass validOn to bracket only rules active on a specific date (the resolver compares against valid_from / valid_to inclusive). Filters compose with AND. Used by admin pricing reports, by ERP / PIM sync flows that mirror group price overrides outward, and by the customer pricing audit. For per-group hydration via the group object, use customerGroups and consume the priceList field on each group. """ customerGroupPriceList( """Restrict to one customer group's rules.""" customerGroupId: ID """Restrict to rules for one product.""" productId: Int """ Date the rule must be active on (compared inclusive against valid_from / valid_to). ISO-8601 date. """ validOn: String """Maximum number of rules to return.""" limit: Int """Offset for pagination.""" offset: Int ): [CustomerGroupPriceRule] """ Page through browse-history entries, newest first. Filters compose with AND: pass customerId to follow one logged-in customer's journey, sessionKey to reconstruct one anonymous (or pre-login) session, or use the date-range pair for timeboxed analytics queries. Used by the 'Recently viewed' surface, by funnel-analytics dashboards, and by abuse / scraping investigations needing the IP + user-agent trail. Note that anonymous and logged-in views can share a sessionKey — joining the two halves of a journey across the login boundary. """ browseHistory( """Restrict to one logged-in customer's history.""" customerId: Int """Restrict to one storefront session's history.""" sessionKey: String """Inclusive lower bound on the page-view timestamp. ISO-8601 datetime.""" startDate: String """Inclusive upper bound on the page-view timestamp. ISO-8601 datetime.""" endDate: String """Maximum number of entries to return.""" limit: Int """Offset for pagination.""" offset: Int ): [BrowseHistory] """ List the billing-only addresses belonging to one customer, in insertion order. Used by the storefront's checkout when a B2B customer has multiple invoice recipients to pick from, by the customer account's 'Invoice addresses' page, and by admin order-creation when routing an invoice to a non-shipping recipient. For shipping addresses (with optional alternate billing in the same row) see addressBookEntries. For the address-rendering templates that turn these fields into a printed block, see addressFormats. """ billingAddresses( """Customer to fetch billing addresses for.""" customerId: ID! """Maximum number of addresses to return.""" limit: Int """Offset for pagination.""" offset: Int ): [BillingAddress] """ Page through the outbound-API job queue. All filters are optional and AND-composable: scope by api / endpoint / jobId for targeted investigations, and by status to split the queue into 'queued' (no error) vs 'failed' (last attempt errored). Used by the admin queue dashboard to surface stuck jobs, by oncall runbooks investigating ERP-sync incidents, and by maintenance jobs that retry or expire failed entries. """ apiProcessQueue( """Filter to one status. Example values: 'queued', 'failed'.""" status: String """Filter to jobs targeting one external API (e.g. 'netvisor').""" api: String """Filter to jobs hitting one endpoint path.""" endpoint: String """Filter to one worker-side job id.""" jobId: String """Maximum number of jobs to return.""" limit: Int """Offset for pagination.""" offset: Int ): [ApiProcessJob] """ List the address-book entries belonging to one customer. Each row is a shipping address with optional alternate-billing fields filled in when the customer wants invoices routed to a different recipient. Used by the storefront's address picker at checkout, by the customer account 'My addresses' page, and by admin order-creation to load a customer's saved addresses. For a customer's standalone billing-only book see billingAddresses; for snapshots of billing/delivery on a finalized order see the order ecosystem instead. """ addressBookEntries( """Customer to fetch addresses for.""" customerId: ID! """Maximum number of entries to return.""" limit: Int """Offset for pagination.""" offset: Int ): [AddressBook] """ List active storefront-domain configurations (status = 1). Pass locale to scope to one language code (matches Domain.code, e.g. 'fi' or 'en'); omit it to enumerate every domain the tenant has enabled. Used by the storefront's bootstrap to pick the active domain's pixel ids and hreflang at render time, by the language switcher to enumerate alternate-language domains, and by the per-domain AI chat-widget bootstrap to load the right assistant + vector-store. For language metadata not specific to a domain, see the languages query. """ domains( """ Restrict to one language code (matches Domain.code, e.g. 'fi', 'sv', 'en'). """ locale: String """Maximum number of domains to return.""" limit: Int """Offset for pagination.""" offset: Int ): [Domain] """ List the modern delivery-address entries for one customer. The response can side-load the legacy AddressBook bridge for entries that were migrated from the old schema — pass includeLegacy=true when the calling code still needs to reach the legacy billing fields on the original row. Used by the storefront's checkout shipping picker on tenants running the modern address split, by the customer-account 'Delivery addresses' page, and by admin order-creation flows. For the legacy single-table address book see addressBookEntries; for the standalone billing book see billingAddresses. """ deliveryAddresses( """Customer to fetch delivery addresses for.""" customerId: ID! """Maximum number of addresses to return.""" limit: Int """Offset for pagination.""" offset: Int """ When true, side-load the legacy AddressBook row referenced by each entry's legacyAddressBookId. When false (default), legacyAddress is left null even if a bridge id exists. """ includeLegacy: Boolean ): [DeliveryAddress] """ Standalone query for one banner's per-day history — equivalent to Banner.historyByDate but reachable without first loading the parent Banner. Used by the admin's stats dashboard when the calling code already knows the banner id and wants the chart in a single round-trip. Returns day-bucketed aggregates ordered newest first. For the banner metadata (title, url, group, image) and lifetime totals, query the banners surface alongside. """ bannerHistory( """Banner to fetch the per-day history for.""" bannerId: Int! """Inclusive lower bound on the banner-history date. ISO-8601 date.""" startDate: String """Inclusive upper bound on the banner-history date. ISO-8601 date.""" endDate: String """Maximum number of day-buckets to return.""" limit: Int """Offset for pagination across day-buckets.""" offset: Int ): [BannerHistoryEntry!]! """ List bank-transfer settlements, newest first. Filters compose with AND: pass ordersId to fetch one order's settlements (typically reached via Order.bankPayments instead), and use the date-range pairs to scope to a posting-date or value-date window for reconciliation reports. Used by the admin's invoice / accounts-receivable tab, by ERP / accounting export jobs, and by 'where's my money?' reports comparing expected and received payments. For card / mobile-wallet payments use ordersNets / ordersVivawallet instead. """ bankPayments( """Restrict to settlements for one order id.""" ordersId: Int """Lower bound for invoicePaymentDate (inclusive). ISO-8601 date.""" invoiceDateFrom: String """Upper bound for invoicePaymentDate (inclusive). ISO-8601 date.""" invoiceDateTo: String """Lower bound for invoicePaymentValueDate (inclusive). ISO-8601 date.""" valueDateFrom: String """Upper bound for invoicePaymentValueDate (inclusive). ISO-8601 date.""" valueDateTo: String """Maximum number of settlements to return.""" limit: Int """Offset for pagination.""" offset: Int ): [BankPayment!]! """ Look up Vivawallet payment records, by order, by transaction UUID, or by Vivawallet's orderCode. Use this for reconciliation against Vivawallet's merchant reports, for chargeback investigations (filter by transactionUuid), or to render the payment-trail block on an admin order detail. For all order-attached payments at once, prefer Order.vivawalletPayments when you're already fetching the order. For Nets-provider notifications see ordersNets / OrderNetsNotification (separate table with a different shape). """ ordersVivawallet( """Filter to records for this single order.""" orderId: Int """ Filter to a specific Vivawallet transaction UUID (chargeback / refund lookup). """ transactionUuid: String """Filter to Vivawallet's orderCode for the payment session.""" orderCode: String """Maximum rows to return.""" limit: Int """Number of rows to skip for pagination.""" offset: Int ): [OrderVivawalletPayment!]! """ List address-format templates, optionally filtered by id. Pass ids to look up specific formats (e.g. the ones referenced by the visible countries); omit ids to enumerate every format the tenant has defined. Used by the address renderer when generating shipping labels, invoice addresses, and admin display strings — paired with each Country's addressFormatId to pick the right template per destination. For the raw address data see addressBookEntries / billingAddresses; this surface only carries the templates. """ addressFormats( """Restrict to specific format ids. Omit to return every format.""" ids: [ID!] """Maximum number of formats to return.""" limit: Int """Offset for pagination.""" offset: Int ): [AddressFormat] """ Page through the audit log, newest first. All filters are optional and AND-composable: pass module to scope to one event source, userId to follow one principal, and success to bracket only the failures (handy for brute-force detection on login modules). Used by the admin's security dashboard, by compliance export jobs, and by oncall when investigating an incident. The entries are append-only — there's no way to amend or delete a row through this surface. """ actionRecorderEntries( """ Restrict to entries from one module (see ActionRecorderEntry.module for example values). """ module: String """Restrict to entries about one user id.""" userId: Int """ Restrict to successful (true) or failed (false) entries. Omit to include all outcomes (including null-status informational rows). """ success: Boolean """ Maximum number of entries to return on this page. Defaults to 25 in the resolver. """ limit: Int """Offset for pagination.""" offset: Int ): ActionRecorderPage! customersPrices(customerId: Int, productId: Int, validOn: String, limit: Int, offset: Int): [CustomerPrice!]! customerPrice(customerId: Int!, productId: Int!, validOn: String): CustomerPrice """ List the per-product step-pricing tiers for one product, ordered by minimum quantity. Used by the storefront's quantity input to show the next price break, by the basket to apply the active tier, and by the quoting flow to compute line totals at the right unit price. For percentage-based discounts gated on customer group / type, see productVolumeDiscounts. For the price-change audit trail, see productPriceHistory. """ productVolumeAddonPrices( """Product to fetch step-pricing tiers for.""" productId: ID! ): [ProductVolumeAddonPriceTier] """ List the group-based volume-discount tiers for one product, in threshold order. Used to render wholesale price ladders on B2B product pages, to apply contract pricing at checkout, and to expose tier eligibility in the admin pricing tab. For per-product step pricing (replace-the-unit-price ladders), see productVolumeAddonPrices. For the price-change audit trail, see productPriceHistory. """ productVolumeDiscounts( """Product to fetch volume-discount tiers for.""" productId: ID! ): [ProductVolumeDiscountTier] """ List the saved Braintree cards for one customer. Used by the storefront's checkout to render the saved-cards picker, by the customer-account 'Payment methods' page to show / remove cards, and by the admin to audit which cards a customer has on file. The token returned here is intended for server-side charging — pass it to Braintree's transaction.sale call. The masked number and card type are cached locally for display purposes only. """ customersBraintreeTokens( """Customer to fetch saved cards for.""" customersId: Int! """Restrict to one card brand (e.g. 'Visa'). Omit for all brands.""" cardType: String """Maximum number of saved cards to return.""" limit: Int """Offset for pagination.""" offset: Int ): [CustomersBraintreeToken!]! """ List blacklist entries, newest first. All filters are optional and AND-composable: pass customerId to fetch one customer's full history, status to scope to active blocks vs. lifted ones, or omit both to walk the tenant's full blacklist for a moderation review. Used by the admin's customer detail view to render the block badge + history, by checkout middleware that reads the latest status to decide whether to short-circuit the order, and by fraud-investigation reports. For per-method bans see Customer.blockPayments / blockShipments; for product-allowlist flows see customersProductsInclusions. """ customersBlacklist( """Restrict to one customer's blacklist history.""" customerId: Int """Restrict to entries with this status code (e.g. 1 = active block).""" status: Int """Maximum number of entries to return.""" limit: Int """Offset for pagination.""" offset: Int ): [CustomerBlacklistEntry!]! } type Mutation { _empty: String """ Create a registered customer account with a password. Email address must be unique across registered customers in the tenant — the resolver throws when the email collides with an existing registered account. Password is bcrypt-hashed before storage. Returns the created Customer including its assigned id. """ createCustomer(input: CreateCustomerInput!): Customer! """ Create a guest customer for one-time checkout, without a password. The resolver sets temporary_customer to the current Unix timestamp so admin tooling can later sweep stale guest rows. Guest customers may share email addresses with each other and with registered customers. Returns the created Customer including its assigned id. """ createGuestCustomer(input: CreateGuestCustomerInput!): Customer! """ Create an order with line items, addresses, and chosen payment + shipping methods. The resolver validates paymentCode against the tenant's MODULE_PAYMENT_INSTALLED configuration and shippingCode against MODULE_SHIPPING_INSTALLED — both are required and free-form or removed-module values are rejected loud (with the error message listing what's available). Status defaults to 1 (Received) unless explicitly set. The customer-side party snapshot is captured on the order so later edits to the customer's profile or saved addresses don't retroactively change the order. To record subsequent status transitions with tracking metadata use updateOrderStatus; to add / edit / remove line items after creation use addOrdersProduct / updateOrdersProduct / removeOrdersProduct. """ createOrder( """ The order to create — placing customer, delivery / billing parties, payment + shipping methods, and line items. """ input: CreateOrderInput! ): Order! """ Update an existing order's current status and, in the same transaction, optionally write carrier tracking metadata. Use this when an ERP, WMS, carrier integration, or operator action advances fulfilment after the order already exists. The mutation validates the order and tenant-local status id, updates orders.orders_status and last_modified, writes trackingCode / trackingLink only when those fields are provided, appends an orders_status_history row, and records changed header fields in orders_history for audit visibility. """ updateOrderStatus( """Order status and optional tracking update to apply.""" input: UpdateOrderStatusInput! ): Order! """ Record a new status-history entry on an order. Use this low-level mutation when a caller needs to append to orders_status_history directly. For the normal fulfilment workflow that updates the current order status and optional tracking fields in one operation, prefer updateOrderStatus. Two side-effect flags: - notifyCustomer: when true, stores customer_notified = 1 on the history row. The caller must send any actual email separately. - updateOrderStatus: when true, also writes the new statusId to orders.orders_status so it becomes the order's current status. The default is false, so omitted calls are history-only. The inserted row is surfaced through the orderStatusHistory read query. Use updateOrderStatus when the caller also needs header-field audit entries in ordersHistory. """ addOrdersStatusHistory(input: OrdersStatusHistoryInput!): OrdersStatusHistoryEntry! """ Create a quote (B2B request-for-pricing) with one or more line items. Captures the requester (customer id or anonymous session id), the requested shipping method, the preferred warehouse, and the line set. The resolver writes the quote header and its QuoteProduct lines in a single transaction. Used by the storefront's quote-request form, by the admin's 'Create quote on customer's behalf' flow, and by integrations that ingest quote requests from external systems. To attach files (briefs, drawings) afterwards see addQuoteFile; to record status transitions see addQuoteStatusHistory. """ createQuote( """The quote to create — header fields plus line items.""" input: CreateQuoteInput! ): Quote! """ Record a new status transition on a quote. Persists the row to quotes_status_history and, when updateNotifications is set, also flips the parent quote's reminder / follow-up flags so the auto-reminder system picks up the new state. Returns the persisted timeline entry, including the assigned id and timestamp. Use this for the admin quote editor's status-change dropdown, for automated workflow steps that move quotes through approval, and for the quote-to-order conversion flow that closes the quote out by moving it to 'Ordered'. To read the timeline see quoteStatusHistory. """ addQuoteStatusHistory( """The status transition to record.""" input: AddQuoteStatusHistoryInput! """ Optional reminder / follow-up flag updates applied to the parent Quote in the same call. """ updateNotifications: QuoteNotificationFlagsInput ): QuoteStatusEntry """ Record a new attachment on a quote. The file is expected to already be uploaded to the tenant's quote-files directory; this mutation persists the filename in quote_files so it shows up in the quote's attachment list. Returns the persisted QuoteFile including its assigned id. """ addQuoteFile( """Quote id to attach the file to.""" quoteId: ID! """Filename of the already-uploaded attachment.""" file: String! ): QuoteFile! """ Upsert a line in a shared basket. Used to add a product to a collaborative basket (sales rep building a draft for a customer, team-mates co-editing a wholesale list) or to overwrite an existing line's full contents in one call. For narrow updates on quantity only, see updateSharedBasketQuantity. """ createOrUpdateSharedBasket( """Line content to upsert.""" input: CreateSharedBasketInput! ): SharedBasketItem! """ Change the quantity on an existing shared-basket line. The storefront's quantity stepper calls this on every increment / decrement, so it's the hot-path mutation for shared-basket edits. Throws when the line is past expiresAt unless requireActive=false. """ updateSharedBasketQuantity( """Quantity update for one (sharedCartId, itemId) line.""" input: UpdateSharedBasketQuantityInput! ): SharedBasketItem! """ Expire a shared-basket line — rewrite the expiresAt timestamp so subsequent requireActive=true reads start refusing. Used by admin 'Force expire' actions and by the periodic stale-basket sweeper. """ expireSharedBasketItem( """Expiry update for one (sharedCartId, itemId) line.""" input: ExpireSharedBasketInput! ): SharedBasketItem! """ Issue a coupon to a specific customer. Verifies the coupon and the customer both exist, then writes a discount_coupons_to_customers row (idempotent — repeated calls don't create duplicates). Used by admin tools that hand out named-account / VIP coupons, by loyalty rule engines that earn coupons for specific customers, and by onboarding flows that issue welcome coupons. Once attached, the coupon's customers[] list (visible via the discountCoupon query) includes this customer, and the customer becomes eligible to redeem the coupon at checkout. """ attachDiscountCouponToCustomer( """The (couponCode, customerId) pair to attach.""" input: DiscountCouponCustomerLinkInput! ): DiscountCouponCustomerLink! """ Detach a coupon from a customer. Returns true on success, false when no link row matched. Used to revoke a previously-issued private coupon, e.g. when the recipient no longer qualifies for the offer or the coupon was attached in error. """ removeDiscountCouponFromCustomer( """The (couponCode, customerId) pair to detach.""" input: DiscountCouponCustomerLinkInput! ): Boolean! """ Attach a new variant selection to an existing order line. Use this after addOrdersProduct (or on an existing line) to record a size/colour/personalisation choice that affects the line's total via pricePrefix. Returns the inserted OrdersProductsVariant. """ addOrdersProductsVariant(input: AddOrdersProductsVariantInput!): OrdersProductsVariant! """ Partially update an existing variant selection. Only the fields in the input payload are written. Useful for correcting an option name or price delta after the order was placed without removing and re-adding the variant. Returns the updated OrdersProductsVariant. """ updateOrdersProductsVariant(id: ID!, input: UpdateOrdersProductsVariantInput!): OrdersProductsVariant! """ Delete a variant selection from an order line. Returns true on success, false when no row matched. Hard delete — does not change the parent line's price; recompute the line price separately if needed. """ removeOrdersProductsVariant(id: ID!): Boolean! """ Add a new line to an existing order. Use this from the admin panel or an ERP integration to extend an order after it has been placed — e.g. to add an upsold accessory, an apology gift, or a manual correction line. Returns the inserted OrdersProduct including its assigned id. To attach configurable-option selections to the new line, follow up with addOrdersProductsVariant once you have the returned id. """ addOrdersProduct(input: AddOrdersProductInput!): OrdersProduct! """ Partially update an existing order line. Only the fields included in the input payload are written; omitted fields are left as-is. Returns the updated OrdersProduct. For variant selections on the line, use updateOrdersProductsVariant instead — they live on a separate table. """ updateOrdersProduct(id: ID!, input: UpdateOrdersProductInput!): OrdersProduct! """ Delete an order line. Returns true on success, false when no row matched the given id. Variants attached to the line are deleted by cascade. This is a hard delete — for refunds prefer adding a 'refund' line via addOrdersProduct, which preserves the audit trail. """ removeOrdersProduct(id: ID!): Boolean! """ Idempotently record a Nets payment notification. Inserts a new orders_nets row on first call; on a duplicate ordersPaytrailId it updates the row with the latest webhook payload (newer notificationId, transactionId, stamp, reference, amount, error). Use this from the Nets webhook handler so retries and out-of-order deliveries from Nets's side don't create duplicate rows. Returns the persisted OrderNetsNotification, or null on a no-op. For Vivawallet, see ordersVivawallet (separate table with a different shape). """ upsertOrdersNetsNotification(input: OrdersNetsNotificationInput!): OrderNetsNotification """ Add a product to a customer's private allowlist. When this is the customer's first inclusion row, the catalog flips to private mode for them — they immediately stop seeing all non-listed products. Used when onboarding a B2B account onto a contract assortment. """ grantCustomerProductAccess(input: CustomerProductInclusionInput!): CustomerProductInclusion! """ Remove one product from a customer's private allowlist. When this leaves the customer with zero inclusion rows, the catalog flips back to public mode for them and they regain visibility into the standard catalog. """ revokeCustomerProductAccess(input: CustomerProductInclusionInput!): Boolean! """ Attach a ProductOption to a product as a custom (per-product) override, with its own required-flag, display position, and input type. Used by the admin product editor when a tenant wants this option to render differently on this specific product than its global default. """ createProductCustomOption(input: CreateProductCustomOptionInput!): ProductCustomOption """ Update an existing per-product option override — typically to change the required flag, reorder, or switch the input rendering between radio / select / checkbox / etc. """ updateProductCustomOption(id: ID!, input: UpdateProductCustomOptionInput!): ProductCustomOption """ Detach a custom option from a product. The underlying ProductOption is untouched; only this product's override is removed. """ removeProductCustomOption(id: ID!): Boolean """ Add a product to a customer's wishlist. Inserts a new row in customers_favorites; the same (customer, product) pair can appear only once per the table's uniqueness constraint, so attempting to re-add an existing favorite errors at the DB layer. Used by the storefront's 'heart' / 'save for later' button on product cards and product detail pages. """ createFavorite(input: AddCustomerFavoriteInput!): CustomerFavorite """ Remove favorites matching the input filter. The filter must include at least one of id / customersId / productsId — the resolver throws when none are provided. Returns true when at least one row was deleted, false when nothing matched. Used by the storefront's 'unsave' button (id or customersId+productsId), by 'clear wishlist' actions (customersId only), and by admin tooling that retires a product and wants to clean up everyone's favorites of it (productsId only). """ deleteFavorite(input: RemoveCustomerFavoriteInput!): Boolean } """ Input for createCustomer — registers a new customer with a password. Fields not marked required can be omitted; the storefront / admin capture the rest at checkout or via the address book later. """ input CreateCustomerInput { """Salutation / gender code (e.g. 'm', 'f'). Optional.""" gender: String """First name.""" firstname: String """Last name.""" lastname: String """ Email address. Must be unique across registered (non-temporary) customers in the tenant. """ emailAddress: String! """ Plaintext password — the resolver bcrypts it before storing. Capped at 128 characters. """ password: String! """Telephone number.""" telephone: String """ Newsletter opt-in flag. true subscribes the customer to the newsletter list. """ newsletter: Boolean """Company name. Set for B2B accounts.""" company: String """Street address line for the initial address-book entry.""" streetAddress: String """VAT / business id. Captured for B2B accounts.""" vatid: String """Postal code for the initial address-book entry.""" postcode: String """City for the initial address-book entry.""" city: String """State / region for the initial address-book entry.""" state: String """Country id (resolves against the countries query).""" countryId: Int """ Two- or three-letter country code, alternative to countryId. The resolver maps it through countries; if countryId is also provided they must match. """ countryCode: String """Zone id within the country.""" zoneId: Int """Free-form delivery instructions captured at registration.""" comments: String } """ Input for createGuestCustomer — provisions a guest account for one-time checkout. No password is required (the platform generates a random one internally so the row can still satisfy the auth schema). Guest emails are not unique across the tenant — multiple guest checkouts can share the same email, and a guest email can also coexist with a registered customer using the same address. """ input CreateGuestCustomerInput { gender: String firstname: String lastname: String """Email address. Not enforced unique for guests.""" emailAddress: String! telephone: String newsletter: Boolean company: String streetAddress: String vatid: String postcode: String city: String state: String """Country id (resolves against the countries query).""" countryId: Int """ Two- or three-letter country code, alternative to countryId. The resolver maps it through countries; if countryId is also provided they must match. """ countryCode: String zoneId: Int comments: String } """ An address-book entry for a customer, returned through Customer.addresses. Holds the shipping fields in the entry_* columns with optional alternate billing fields in the billing_* columns of the same row — same shape as the standalone addressBookEntries query, just hydrated as part of the customer record. For the dedicated query see addressBookEntries; for billing-only address books see billingAddresses. """ type Address { """Database row id of the address-book entry (address_book_id).""" id: ID! """Salutation / gender code on the shipping side.""" gender: String """Company name on the shipping side.""" company: String """Recipient first name on the shipping side.""" firstname: String! """Recipient last name on the shipping side.""" lastname: String! """Street address on the shipping side.""" streetAddress: String! """VAT id on the shipping side. Captured for B2B shipping documents.""" vatid: String """Postcode on the shipping side.""" postcode: String! """City on the shipping side.""" city: String! """State / region on the shipping side.""" state: String """Country id on the shipping side.""" countryId: Int """Zone id on the shipping side.""" zoneId: Int """Delivery instructions captured at address creation.""" comments: String """ Alternate billing-side street address. Null means billing uses the shipping side. """ billingStreetAddress: String """Alternate billing first name.""" billingFirstname: String """Alternate billing last name.""" billingLastname: String """Alternate billing postcode.""" billingPostcode: String """Alternate billing city.""" billingCity: String """ Alternate billing country id (string-typed on this surface, vs. shipping's int). """ billingCountryId: String """Alternate billing company name.""" billingCompany: String """Alternate billing VAT id.""" billingVatid: String """Alternate billing zone id.""" billingZoneId: String """Alternate billing state / region.""" billingState: String """ E-invoice operator id (Finnish Verkkolasku operator). Used for routing electronic invoices through an operator. """ billingOperatorId: Int """ E-invoice address (Finnish OVT-tunnus / EDI). Paired with billingOperatorId for e-invoice delivery. """ billingInvoiceAddress: String """Free-form reference / PO number printed on the invoice.""" billingReference: String """ Whether the storefront enforces a non-empty reference at checkout for this address. """ billingReferenceRequired: Boolean } """ A customer of the tenant — registered shopper, B2B account, or guest one-time-checkout record. The same row holds individual and B2B customers (distinguished via the type field), and guest checkouts reuse the table with temporaryCustomer set so the storefront can later upgrade them to registered accounts. Carries identity (email, name, telephone), behaviour flags (newsletter, freeShipping threshold, payment / shipping blocks), pointers to the customer's saved addresses (addresses field, default ids), the activity-stats sidecar (info field), and ERP-facing ids (apiId, apiBillingId). For login / failure counters see CustomerInfo; for per-customer pricing see customerPrice / customersPrices; for group-based pricing see customerGroups. """ type Customer { """Database row id of the customer.""" id: ID! """Salutation / gender code (e.g. 'm', 'f'). Free-form, tenant-defined.""" gender: String """First name.""" firstname: String """Last name.""" lastname: String """ Login email. Unique across registered customers in the tenant; guest customers (temporaryCustomer = true) may share email addresses with each other and with registered accounts. """ emailAddress: String """ Pointer to the customer's default address-book entry — the address the storefront pre-selects at checkout. """ defaultAddressId: Int """ Pointer to the address-book entry used as the default delivery destination. May differ from defaultAddressId when the customer ships to a different address than they bill to. """ defaultDeliveryAddressId: Int """Telephone number.""" telephone: String """Newsletter subscription state. true = opted in.""" newsletter: Boolean """ Free-shipping order-value threshold. Orders over this value (in the tenant's default currency) ship free; 0 = no per-customer override (the global rule applies). Per-customer free shipping; not a percentage discount. """ freeShipping: Float """ Whether this customer record is also an admin user. Stored as int — 0 = regular customer, non-zero = admin (mostly legacy / ERP-staff records). """ admin: Int """ Customer category. Example values: - 0: temporary / anonymized customer (system-managed; placeholder for guest checkouts and soft-deleted records) - 1: registered individual customer (default for normal end-user accounts) - 2: business / B2B customer (typically has VAT id and company populated) """ type: Int """ Customer-group memberships. Stored as a JSON-encoded array of group_id strings (e.g. '["1","5"]'). Empty array means the customer is in no groups. Group definitions live in the customer_groups table — resolve human-readable descriptions, discount type, and customer-type / B2B gating via the customerGroups query. The customerPrice resolver consults this list when choosing the lowest applicable group price for a product. """ group: String """ true once the customer has confirmed their email and the account is enabled for purchasing. Used for double-opt-in and B2B account moderation flows. """ approved: Boolean """ Counter of orders that failed delivery (returned to sender, undeliverable address, etc.). Useful as a fraud / address-quality signal. """ undeliveredOrders: Int """ Comma-separated list of payment-module ids the customer is barred from using at checkout (e.g. 'invoice,banktransfer'). Empty / null means no restrictions. Set by admins on accounts with payment risk. """ blockPayments: String """ Comma-separated list of shipping-module ids the customer is barred from using at checkout. Empty / null means no restrictions. """ blockShipments: String """ true when this row is a guest-checkout / placeholder record rather than a registered account. The underlying column stores a Unix timestamp of when the guest record was created (so admin tooling can sweep stale guests); this field is the truthy/falsy projection of that timestamp. """ temporaryCustomer: Boolean """ Whether the welcome / confirmation email has been dispatched to the customer. """ emailSent: Boolean """ External (ERP / PIM) identifier for the customer record, captured when the customer is mirrored from a third-party system. """ apiId: String """ External identifier for the customer's billing entity in the ERP. Distinct from apiId — large customers can have one billing record covering multiple shipping accounts. """ apiBillingId: Int """ Whether the customer has consented to receive WhatsApp notifications (order updates, abandoned-cart reminders). """ grantWhatsapp: Boolean """ E-invoice operator id captured on the customer record itself (a customer-level default that propagates onto billing addresses). """ billingOperatorId: Int """ E-invoice address (Finnish OVT-tunnus / EDI) captured on the customer record itself. """ billingInvoiceAddress: String """Customer-level invoice reference / PO number default.""" billingReference: String """ Whether the customer's invoices require a non-empty reference at checkout. """ billingReferenceRequired: Boolean """ Activity-stats sidecar (last login, login counters, account creation date, etc.). Backed by the customers_info table — see CustomerInfo for field-level docs. """ info: CustomerInfo """ Address-book entries for this customer, hydrated alongside the customer record. For an independent paginated view see addressBookEntries. """ addresses: [Address] } """ Per-customer login and lifecycle counters tracked outside the main customers row. The customers_info table holds slow-changing meta about a customer account — when they last logged in, how many times, how many failed attempts, when the account was created and last edited, plus optional marketing-source attribution and notification preferences. Used by the admin's customer detail view to surface 'Last login' and 'Account age' badges, by security tooling that watches failed-login counters, and by any onboarding / re-engagement automation that needs to know how stale an account is. For a customer's full profile + addresses see the customer query; this surface is the activity-stats side car. """ type CustomerInfo { """ Timestamp of the customer's most recent successful login. Null for accounts that have never logged in (e.g. admin-created records). """ dateOfLastLogon: String """ Total successful logins by this customer over the lifetime of the account. """ numberOfLogons: Int """ Total failed-login attempts since the last successful login. Used by anti-brute-force tooling — typically reset on successful auth. """ numberOfFailedLogins: Int """ Timestamp of the most recent failed-login attempt for this account. Null when no failed attempts have been recorded. """ dateOfLastLoginFail: String """Timestamp the account was created.""" dateAccountCreated: String """ Timestamp the account was last edited (profile update, password change, address change). """ dateAccountLastModified: String """ Marketing-source attribution id captured at registration — referrer / campaign id when the tenant runs source tracking. Null when the tenant doesn't track sources or the source was unknown. """ sourceId: Int """ Whether the customer has opted in to global product notifications (back-in-stock alerts, restock notices, etc.). Stored as int (1=on, 0=off). """ globalProductNotifications: Int } """ A product available for sale in the catalog. This type is intentionally thin — it carries the headline fields used to render product cards and the basic detail header (id, name, model, image, price, descriptions, material). Richer product surfaces hang off other queries: - Configurable options + values: productAttributes - Variant-level stock + per-variant pricing: productStock - Per-warehouse stock split: warehouseStock - Price history: productPriceHistory - Group-based price overrides: customerGroupPriceList - Per-customer pricing: customerPrice - Volume / step pricing: productVolumeAddonPrices, productVolumeDiscounts - Image gallery (variant-aware): productExtraImages - PDFs / CAD / lighting profiles / non-image media: productsMedia - Category memberships: productsToCategories - Per-product custom-option overrides: productsCustomOptions The Product.description and shortDescription fields come from a per-language products_description row picked by the languageId argument on the product query. The products listing query returns rows in the tenant's primary language only. """ type Product { """ Database row id of the product (products_id). Used as the productId in every related product surface listed above. """ id: ID! """ Display name shown to shoppers — falls back to the product model when the localized name is empty. """ name: String! """ Manufacturer / vendor SKU or model identifier. Stable across languages and used as the join key into ERP / PIM systems. """ model: String """ Filename of the main product image, served from the tenant's product-image directory. For the variant-aware image gallery use productExtraImages; for non-image media use productsMedia. """ image: String """ Catalog base price in the tenant's default currency, exclusive of VAT. Per-customer / per-group / volume-tier prices may override this at checkout — see customerPrice / customerGroupPriceList / productVolumeAddonPrices. """ price: Float! """ Long-form HTML description rendered on the product detail page. Comes from products_description for the requested languageId. """ description: String """ Plain-text short-description used in product cards, listings, and meta descriptions. Comes from products_description for the requested languageId. """ shortDescription: String """ Material / composition string (e.g. 'cotton 80%, polyester 20%'). Comes from products_description for the requested languageId; null when the tenant doesn't capture material per product. """ material: String } """ An additional image attached to a product, beyond the main image stored on the Product type. Used to render the gallery / carousel on product detail pages: alternate angles, in-use shots, packaging, lifestyle photography. Optionally variant-specific via optionId — e.g. a colour variant can have its own gallery, so the storefront swaps images when the shopper picks a different colour. For non-image attachments (PDFs, CAD files, videos) see productsMedia, which is a broader media-attachment surface with an explicit type and category. """ type ProductExtraImage { """Database row id of the extra-image entry (products_extra_image_id).""" id: ID! """ Free-form code captured at upload time, used by tenants for sorting / matching against ERP image references. Null when the tenant doesn't use codes. """ code: String """ Filename or relative URL of the image, served from the tenant's product-image directory. """ url: String """ Display sort order within the product's gallery. Lower values render first. """ sort: Int """ Variant option-value id this image is tied to (matches a row in products_options_values). When set, the storefront shows this image only when the shopper has that variant selected. 0 = applies to all variants. """ optionId: Int """Localized caption / alt text for the image.""" description: String } """ A grouping label for product media files — for example 'manuals', 'datasheets', 'IES', 'PDF'. Tenants define their own categories; there's no global enum. Categories show up as section headers when the storefront renders the product attachments block. """ type ProductMediaCategory { """Database row id of the category.""" id: ID! """ Tenant-defined free-form code (e.g. 'manuals', 'IES', 'responsibility'). Used internally for grouping and ERP matching. """ code: String """ Display sort order of the category itself within the product's media block. """ sortOrder: Int """Localized human-readable category name shown to shoppers.""" name: String } """ A media attachment on a product — PDF datasheets, CAD files, lighting profiles, additional documents, video links, anything beyond the main image and gallery. Each file has a type indicating its format and an optional category for storefront grouping. For image-specific gallery shots tied to product detail pages or variants, prefer productExtraImages — it has variant-awareness via optionId. ProductMedia is the broader catch-all surface used by the storefront's "Documents & downloads" section and by the Akeneo / PIM sync pipelines (see ProductMediaSyncer.php). """ type ProductMedia { """Database row id of the media entry (products_media_id).""" id: ID! """Product this media belongs to.""" productId: ID """ Id of the parent ProductMediaCategory, or null for uncategorised media. """ categoryId: Int """ Filename of the attachment, served from the tenant's product-media directory. """ file: String """Display sort order within the parent category.""" sortOrder: Int """ File-type label. Production has both CamelCase and lowercase variants (historical drift). Example values seen across tenants: - 'PDF' / 'pdf' — datasheets, manuals - 'JPG' / 'JPEG' / 'jpeg' / 'PNG' / 'GIF' / 'WEBP' / 'AVIF' / 'SVG' — images - 'IES' — lighting profiles (photometric data) - 'DOC' / 'DOCX' / 'XLS' / 'XLSX' / 'TXT' — office documents - 'DWG' / 'DXF' / 'DWF' — CAD files - 'LESS' — stylesheet partial The full whitelist (with file-extension prefixes) lives in ProductMediaSyncer.php in aicc-mvc. """ type: String """ Human-readable size label (e.g. '2.4 MB'). Pre-formatted at upload time. """ size: String """ Localized description / caption shown alongside the file in the storefront. """ description: String """ Resolved parent category, included when includeCategory=true on the query. """ category: ProductMediaCategory } """ A country supported by the tenant for shipping or billing addresses. Countries are the top-level reference data behind every address picker on the storefront and admin: registration forms, checkout shipping / billing destinations, the customer's address book, and the admin order builder all read from this surface. Each country carries the format-template id used to render its addresses (Country.addressFormatId resolves against addressFormats), the default currency a shopper from that country sees on first visit, and optional duty fields used for cross-border tax / customs calculation. """ type Country { """Database row id of the country.""" id: ID! """ Localized country name shown in country dropdowns (e.g. 'Finland', 'United States'). """ name: String! """ Two-letter ISO 3166-1 alpha-2 country code (e.g. 'FI', 'US'). Used for storefront locale resolution and most external integrations. """ iso2: String! """ Three-letter ISO 3166-1 alpha-3 country code (e.g. 'FIN', 'USA'). Used by some shipping carriers and tax services. """ iso3: String! """ Default customs-duty rate applied to imports into this country, as a percentage. Used by the cross-border price calculator; informational on countries the tenant doesn't ship to. """ dutyRate: Float """ De minimis threshold — order value below which no duty applies, in the tenant's default currency. Null when not configured. """ dutyLimit: Float """ Id of the AddressFormat used to render addresses in this country (street order, postcode placement, state line, etc.). Resolves against the addressFormats query. """ addressFormatId: Int """ Id of the default Currency a shopper from this country sees on first visit, before they explicitly switch. Resolves against the currencies query. """ defaultCurrencyId: Int } """ A logical grouping of configuration settings shown as a section in the admin's configuration UI (e.g. 'Email Options', 'Tax Options', 'Stock Settings', 'Module — Payment'). Groups exist purely to organize the admin-side rendering of the underlying key/value Configuration rows — the storefront and backend code read individual settings by key, not by group. """ type ConfigurationGroup { """Database row id of the group (configuration_group_id).""" id: ID! """ Display title shown as the section header in the admin configuration UI (e.g. 'Email Options'). """ title: String """Helper text shown below the section title in the admin UI.""" description: String """ Display order of the group within the admin configuration UI. Lower values render first. """ sortOrder: Int """ Settings that belong to this group, populated when the parent group is fetched via configurationGroupsWithSettings. The configurations field is empty when the group is reached through configurationGroups (which doesn't eagerly hydrate its children). """ configurations: [Configuration] } """ One configuration setting — a key/value pair that controls platform behaviour. Each row has a stable string key (e.g. 'STORE_NAME', 'MODULE_PAYMENT_NETS_INSTALLED') that the storefront and backend code read at runtime, plus a free-form value, an admin-side display title, and a helper description. useFunction and setFunction are PHP function names invoked by the admin UI to render the value (e.g. format a checkbox vs a free text input) and to validate / transform input on save. Settings that don't need custom rendering leave both null. """ type Configuration { """Database row id of the setting (configuration_id).""" id: ID! """Display title shown next to the input in the admin configuration UI.""" title: String """ Stable lookup key the storefront and backend read at runtime (e.g. 'STORE_NAME', 'MAX_DISPLAY_BESTSELLERS', 'MODULE_PAYMENT_NETS_INSTALLED'). """ key: String! """ Current value, stored as a free-form string. The storefront / backend casts it to the expected type at read time. """ value: String """Helper text shown below the input in the admin configuration UI.""" description: String """Id of the parent ConfigurationGroup (configuration_group_id).""" groupId: Int """ Display order of the setting within its group. Lower values render first. """ sortOrder: Int """ Timestamp of the most recent edit. Useful for change-tracking dashboards. """ lastModified: String """Timestamp the setting was first inserted.""" dateAdded: String """ PHP function name the admin UI calls to render the input control (e.g. for checkbox vs textarea vs select). Null when the setting uses the default text input. """ useFunction: String """ PHP function name the admin UI calls to validate / transform input on save. Null when no transformation is needed. """ setFunction: String """ Resolved parent group, populated when the configurations query is filtered by groupId. """ group: ConfigurationGroup } """ An admin-facing error notification. The platform writes a row whenever a background job, integration sync, or critical pipeline raises an error worth surfacing to operators (failed ERP sync, payment-gateway timeout, cron job exception, etc.). The admin dashboard renders the un-cleared rows in an inbox so on-duty operators can investigate and dismiss them once handled. emailErrorSent records whether the platform also dispatched the notification email; errorCleared records whether an operator has acknowledged it. The two flags can move independently — an entry may be emailed but never cleared, cleared without emailing, etc. """ type AdminNotification { """Database row id of the notification (notification_id).""" id: ID! """ Free-form error description written by the producing module. Typically a one-line summary plus the exception message. """ errorText: String! """Timestamp the notification was raised.""" createdAt: String! """ Whether the platform has dispatched the notification email to operators. """ emailErrorSent: Boolean! """Whether an operator has acknowledged / dismissed the notification.""" errorCleared: Boolean! } """ An audit row recording that an admin created (or otherwise mutated) a product. The platform writes one entry per admin-driven product change so the admin product editor's history view, the 'created by …' badge on a product's detail page, and any compliance reporting can attribute the change to a real user. Distinct from productPriceHistory — this surface tracks the lifecycle events of a product entity (creation, edits, etc.), while productPriceHistory tracks just price changes with their old/new values. Distinct from actionRecorderEntries — this surface is product-specific and write-tracked at the model layer, while actionRecorder is a general-purpose audit log. """ type AdminTrackingCreate { """Database row id of the tracking entry.""" id: ID! """Product the entry is about.""" productId: Int! """ Id of the admin who performed the action. Null for system-generated entries (e.g. ERP sync writes). """ userId: Int """ Username of the admin, captured at write time so the row stays readable if the admin is later deleted. """ userName: String """ Action performed on the product. Examples seen in production: - 'create' — product was created - 'update' — product was edited - 'delete' — product was deleted Tenants may extend; treat as free-form code rather than a fixed enum. """ action: String """Timestamp the action was recorded.""" dateAdded: String! } """ An admin user — someone with login access to aicc-admin. Holds the account's identity (username, email), authentication settings (2FA), and the RBAC scope that decides which admin pages and modules they can open and whether they're allowed to write or only to read. superadmin trumps the per-module grantAccess list — superadmins see every module regardless. writePermission is checked at the module level to gate save / delete actions independently of read access. For per-action audit trails of what an admin actually did with these permissions, see actionRecorderEntries. """ type Administrator { """Database row id of the administrator.""" id: ID! """Login username.""" userName: String! """Email used for password resets and operator notifications.""" email: String """ Whether 2FA (TOTP) is enabled. When true and the secret column is set, login requires a 6-digit code from an authenticator app on top of username + password. """ twoFactor: Boolean """ Whether the admin is a super-administrator. Superadmins bypass the grantAccess RBAC list and can also manage other admin accounts. """ superadmin: Boolean """ Whether the admin can perform write operations (create / update / delete). When false, the account is read-only across whichever modules they can open. """ writePermission: Boolean """ RBAC list of admin modules the user can access. Stored as a semicolon-delimited string of module ids. Example value: 'module_orders;module_customers;module_products' grants access to the orders, customers, and products admin pages. Ignored for superadmins (they see every module). The module id list is defined by aicc-admin and varies by tenant. """ grantAccess: String } """ A currency the storefront supports for display and order placement. Tenants enable currencies they want shoppers to be able to switch between (e.g. EUR + SEK + USD), and one currency is configured as the primary in the platform settings — that's the currency used for stock prices, order totals stored in DB, and accounting reports. All non-primary currencies carry an exchange-rate value that's refreshed by a backend job; the storefront multiplies primary-currency prices by this rate at render time when the shopper picks a non-primary currency. Display formatting (currency symbol, where it sits relative to the number) lives in symbolLeft / symbolRight. """ type Currency { """Database row id of the currency.""" id: ID! """ Display name of the currency (e.g. 'Euro', 'US Dollar'). Used in the storefront's currency switcher. """ title: String """ISO 4217 three-letter currency code (e.g. 'EUR', 'USD', 'GBP').""" code: String """ Symbol rendered before the amount when formatting prices (e.g. '$' for USD). Empty when the currency uses a right-side symbol. """ symbolLeft: String """ Symbol rendered after the amount when formatting prices (e.g. ' €' for EUR in some locales). Empty when the currency uses a left-side symbol. """ symbolRight: String """ Exchange rate against the tenant's primary currency. Multiply a primary-currency amount by this value to convert it into this currency at the rate captured at lastUpdated. """ value: Float """ Timestamp of the last exchange-rate refresh. The platform's FX-rate sync job updates this when it pulls fresh rates. """ lastUpdated: String } """ A party on an order — the customer, delivery recipient, or billing entity. Captured as a snapshot at the moment of purchase: later edits to the customer's saved AddressBook / BillingAddress entries do not retroactively change finalised orders. Each Order has three of these (customer / deliveryAddress / billingAddress) so the same base shape carries the placing-customer info, where to ship, and where to invoice. """ type OrderParty { """ Recipient name (formatted firstname + lastname or the company contact name) at order time. """ name: String! """ Company name on the party. Set for B2B parties; null on personal-shopper deliveries. """ company: String """VAT / business id on the party. Captured for B2B documents.""" vatId: String """Street address line at order time.""" streetAddress: String! """City at order time.""" city: String! """Postal code at order time.""" postcode: String! """ State / region at order time. Used in countries that need it (US, AU, etc.). """ state: String """ Country name (resolved from the country id at order time and snapshotted as a string). """ country: String! """Telephone number at order time.""" telephone: String """ Email address at order time. May differ from the placing customer's current email if they update their account later. """ emailAddress: String """ AddressFormat id at order time — drives the rendering template used for shipping labels and invoice addresses (resolves against the addressFormats query). """ addressFormatId: Int """ E-invoice address (Finnish OVT-tunnus / EDI). Set for parties using e-invoice routing; null for paper / email invoicing. """ invoiceAddress: String """ E-invoice operator id (Finnish Verkkolasku operator). Paired with invoiceAddress for e-invoice delivery. """ operatorId: Int """Free-form invoice reference / PO number captured at checkout.""" reference: String } """ A purchase placed by a customer — the central record in the order pipeline. Captures everything snapshotted at checkout time: who placed the order (customer / customerId / customerName / customerEmail), where it ships and bills (deliveryAddress / billingAddress), the chosen payment and shipping methods (paymentMethod / paymentCode / shippingMethod / shippingCode + their module-specific fields), the current status, the discount applied at the order level, and the fulfilment metadata that gets populated as the order moves through pick-and-pack and dispatch (deliveryQuantity / weight / dimensions, shippingLocation*, trackingCode / trackingLink). To create an order use createOrder; to update the current status and fulfilment tracking fields use updateOrderStatus; to add / edit / remove line items use the addOrdersProduct / updateOrdersProduct / removeOrdersProduct mutations. Line items hang off Order.items (mirroring the orderItems query); totals breakdown off Order.totals (mirroring orderTotals); coupons applied at checkout off Order.discountCoupons; and provider-specific payment notifications off Order.bankPayments / vivawalletPayments / netsNotifications depending on the payment module used. """ type Order { """Database row id of the order (orders_id).""" id: ID! """ Id of the placing customer. Null for orders placed by a customer record that's since been deleted. """ customerId: Int """ Display name of the placing customer at order time (typically firstname + lastname). Snapshotted so the order stays readable if the customer record is later renamed. """ customerName: String! """ Email of the placing customer at order time. Snapshotted so transactional emails can still be re-sent even if the customer's account email later changes. """ customerEmail: String! """ Customer-side party snapshot (the placing party). Same shape as deliveryAddress / billingAddress for symmetric rendering. """ customer: OrderParty """ Delivery (ship-to) party snapshot at order time. Drives the shipping label. """ deliveryAddress: OrderParty """ Billing (invoice) party snapshot at order time. Drives the invoice header. """ billingAddress: OrderParty """ Numeric status id. Example values across tenants (defaults; tenants extend): - 1: Received - 2: Packing - 3: Shipped - 9: Under investigation - 10: To be picked up - 11: Packed - 15: Awaiting payment - 16: Cancelled - 17: Returned - 21: Uncollected - 23: Backorder Tenants may extend this set with custom statuses (e.g. payment-provider intermediate states, test orders). Always resolve dynamically via the orderStatuses query rather than hardcoding ids. """ status: Int! """ISO timestamp of when the order was placed.""" datePurchased: String """ ISO timestamp of the most recent edit to the order. Touched by status transitions, line edits, and totals recomputation. """ lastModified: String """ISO 4217 currency code the order was placed in.""" currency: String """ Exchange rate (vs the tenant's primary currency) captured at order time. Used to convert the order's foreign-currency totals back into the primary currency for accounting reports. """ currencyValue: Float """ Display label of the chosen payment method as shown to the shopper at checkout (e.g. 'Verkkopankki', 'Postiennakko - 4.90 EUR'). Localized — varies per tenant and language. """ paymentMethod: String """ Code identifying the payment module behind the order. Range seen in production includes module names ('NetsBank', 'Cod', 'NetsCreditcard'), Finnish identifiers ('Lasku', 'Nouto'), snake_case keys ('paylane_paypal'), and historical codes from modules since removed ('WalleyInvoice', 'PaylaneCard', 'BraintreeCard'). For new orders it must match the tenant's MODULE_PAYMENT_INSTALLED list; older orders may carry codes that are no longer in the active list. Use this for routing, reconciliation, and reports. """ paymentCode: String """ Display label of the chosen shipping method as shown to the shopper at checkout (e.g. 'Posti - Noutopiste'). Localized; may include a price suffix. """ shippingMethod: String """ Code identifying the shipping module behind the order. Range seen in production includes module-pair forms ('flat_flat', 'matkahuolto_matkahuolto'), bare module names ('Smartpost', 'Flat'), and historical codes. For new orders it must match the tenant's MODULE_SHIPPING_INSTALLED list. """ shippingCode: String """ Shipping cost charged on the order, in the order's currency. May be 0 when the order qualified for free shipping (via Customer.freeShipping threshold, a coupon, or a customer-group rule). """ shippingCost: Float """Number of physical packages the order ships in (defaults to 1).""" deliveryQuantity: Int """Total package weight in kilograms (decimal, 4-digit precision).""" deliveryWeight: Float """Package length in centimetres.""" deliveryLength: Float """Package width in centimetres.""" deliveryWidth: Float """Package height in centimetres.""" deliveryHeight: Float """ Free-form delivery notes captured at checkout (e.g. 'leave at front door', 'call before arrival'). """ deliveryComments: String """ Pickup-point identifier when the chosen shipping method is a parcel locker / pickup-point service. Empty/0 for home delivery. """ shippingLocationId: String """Display name of the chosen pickup point (e.g. 'R-kioski Kamppi').""" shippingLocationName: String shippingLocationStreetAddress: String shippingLocationPostcode: String shippingLocationCity: String """ Internal id passed to the chosen payment module's API for reconciliation. """ paymentApiId: String """ Payment provider key. Coarser grouping than paymentCode (e.g. 'nets', 'vivawallet'). """ paymentProvider: String """ Free-form payment field captured at checkout (module-specific extra payload). """ paymentField: String """Module-specific payment input payload as captured at checkout.""" paymentInput: String """ Payment-term identifier when the order uses an invoice with terms (e.g. '14 NETTO'). """ paymentTermId: String """ Internal id passed to the chosen shipping module's API for label generation and tracking. """ shippingApiId: String """ Term identifier within the shipping module (e.g. selected service tier). """ shippingApiTermId: String """Cost-centre id used for accounting allocation of shipping costs.""" shippingCostCentreId: String """Carrier-contract identifier used to negotiate shipping rates.""" shippingContractId: String """Carrier name on the contract (Posti, DHL, Schenker, etc.).""" shippingContractCourier: String """ Warehouse code identifying which warehouse the order ships from when the tenant runs multi-warehouse fulfilment. Resolves against the warehouses query. """ warehouseCode: String """ How discountAmount is applied at the order level. Same enum as on coupons and customer groups: 'fixed', 'percent', or 'shipping'. Defaults to 'fixed'. """ discountType: String """Order-level discount value, interpreted according to discountType.""" discountAmount: Float """Carrier tracking code. Populated once the order has been shipped.""" trackingCode: String """ Full tracking URL the customer can click to follow the parcel. May embed the tracking code in the path. """ trackingLink: String """ True (1) once the order-confirmation email has been queued/sent to the customer; 0 means not sent. """ customerNotifiedFlag: Int """ Trolley-locker workflow flag (warehouse pick-and-pack staging). 0 = not picked, non-zero = picked / staged. DECIMAL(2,1) in the DB. """ trollyLocker: Float """ Payment-provider transaction id (the id the bank/PSP returned). Used for reconciliation against statements. """ transactionId: String """IP address the order was placed from (used for fraud / risk scoring).""" ipAddress: String """ISP or geo-org of the placing IP (when resolved).""" ipIsp: String """ Newline-separated list of attached PDF filenames (invoice, packing slip, return label, etc.) stored in the tenant's order-document directory. """ pdfFiles: String """ Customer-supplied reason text when the order was returned. Null when the order was not returned. """ returnReason: String """ Marketing / referral attribution token captured at checkout (UTM tag, click id, etc.). """ referralId: String """ External ERP / marketplace order id, when this order originated from or was synced to an external system. """ ordersApiId: String """ Outbound-API send status: 0 = not yet exported to the ERP, 1 = exported successfully, larger values are tenant-specific intermediate states. """ apiSent: Int """ Order-creation push-status flag: 1 = order-confirmation email has been emitted; 0 = not yet. """ emailSent: Int """Last error / status message from the ERP push (null on success).""" apiMessage: String """ISO timestamp of the apiMessage.""" apiMessageDate: String """Last error / status message from the ERP order-status push.""" apiOrderStatusMessage: String """ISO timestamp of the apiOrderStatusMessage.""" apiOrderStatusMessageDate: String """ Resolved packing rules (JSON-encoded) applied when the order was picked. Null when no per-order packing rules were attached. """ ordersPackingRulesJson: String """ Coupons that were applied to this order at checkout. Empty when no coupon was used. Resolves via the discount_coupons_to_orders join table. """ discountCoupons: [DiscountCoupon] """ All purchased lines on this order, including their selected variants. Equivalent to the orderItems query but resolves in the same round-trip when you're already fetching the Order. """ items: [OrderItem!]! """ Totals breakdown for this order, optionally filtered to one class. Equivalent to the orderTotals query but resolves in the same round-trip. """ totals(class: String): [OrderTotal!]! """ Bank-transfer settlements applied to this order. Empty when the order was paid by another method (card, wallet) or hasn't been paid yet. """ bankPayments: [BankPayment!]! """ Vivawallet payment records linked to this order. Empty for orders paid via other providers (Nets, Cod, invoice). """ vivawalletPayments: [OrderVivawalletPayment!]! } """ Input for one party (customer / delivery / billing) when creating an order. Either passed in full to capture a fresh address snapshot, or omitted in favour of the deliveryAddressId / billingAddressId pointers on CreateOrderInput which load the saved AddressBook entry. """ input OrderPartyInput { """Recipient name (firstname + lastname or company contact).""" name: String """Company name. Set for B2B parties.""" company: String """VAT / business id.""" vatId: String """Street address line.""" streetAddress: String """City.""" city: String """Postal code.""" postcode: String """State / region.""" state: String """Country id (resolves against the countries query).""" countryId: Int """ Two-letter country code, alternative to countryId. The resolver normalises this to a country id. """ countryCode: String """Telephone number.""" telephone: String """Email address.""" emailAddress: String """AddressFormat id used for rendering. Resolves against addressFormats.""" addressFormatId: Int """E-invoice address (Finnish OVT-tunnus / EDI).""" invoiceAddress: String """E-invoice operator id (Finnish Verkkolasku operator).""" operatorId: Int """Free-form invoice reference / PO number.""" reference: String } """ One configurable-option choice on a CreateOrderItemInput line. Multiple variants stack on the same line, each contributing a price delta interpreted via pricePrefix. Mirrors the OrderItemVariant read shape. """ input CreateOrderItemVariantInput { """ Product-option id this choice is for (matches a row in the catalog options table). """ optionId: Int! """Display label of the option as shown to the shopper (e.g. 'Size').""" optionName: String! """Selected value as shown to the shopper (e.g. 'XL').""" optionValue: String! """ Price delta this choice contributes, interpreted via pricePrefix. Defaults to 0. """ price: Float = 0 """ How the price delta is applied: '+' (additive, default) or '' (replacement). """ pricePrefix: String = "+" } """ One line on a createOrder call. Most fields mirror the OrderItem read shape; the resolver writes the captured snapshot into orders_products at the moment of order creation, so later catalog edits don't change the line. """ input CreateOrderItemInput { """Product id from the catalog. Null for free-form / fee / refund lines.""" productId: Int """Optional SKU captured at order time.""" sku: String """Product model number captured at order time.""" productModel: String """EAN / barcode captured at order time.""" productEan: String """Display name to write on the receipt and admin order detail.""" productName: String! """Unit price actually charged, in the order's currency.""" price: Float! """ Catalog price at the time of order, before any per-customer / coupon adjustments. Useful for computing realised discount per line. """ originalPrice: Float """Tax rate applied to this line as a percentage (e.g. 24.0 = 24% VAT).""" tax: Float """Quantity ordered.""" quantity: Int! """Comma- or JSON-encoded snapshot of the variant stock-id selection.""" stockVariants: String """Line type ('item' | 'refund' | tenant-defined). Defaults to 'item'.""" lineType: String = "item" """Free-form line comment (gift message, configuration notes).""" comment: String = "" """Module-specific custom-options payload (JSON-encoded).""" customOptions: JSON """Configurable-option choices selected on this line.""" variants: [CreateOrderItemVariantInput!] } """ Input for createOrder. Use deliveryAddressId / billingAddressId to reference the customer's saved AddressBook entries (resolved at call time and snapshotted onto the order), or pass deliveryAddress / billingAddress as inline OrderPartyInput when the order is for an ad-hoc address that shouldn't land in the customer's saved book. """ input CreateOrderInput { """Placing customer id. Required.""" customerId: Int! """ Saved AddressBook id to use as the delivery address. Mutually exclusive with deliveryAddress. """ deliveryAddressId: Int """ Saved AddressBook id to use as the billing address. Mutually exclusive with billingAddress. """ billingAddressId: Int """ Customer-side party snapshot. Optional — falls back to the customer record. """ customer: OrderPartyInput """Inline delivery party. Mutually exclusive with deliveryAddressId.""" deliveryAddress: OrderPartyInput """Inline billing party. Mutually exclusive with billingAddressId.""" billingAddress: OrderPartyInput """Initial status id. Defaults to 1 (Received).""" status: Int = 1 """Operator comment recorded on the initial status-history entry.""" statusComment: String """ISO 4217 currency code. Defaults to 'EUR'.""" currency: String = "EUR" """ Exchange rate against the tenant's primary currency. Defaults to 1 (when currency matches the primary). """ currencyValue: Float = 1 """ Display label for the chosen payment method as shown to the shopper at checkout (e.g. 'Verkkopankki', 'Pay by paypal'). Required and non-empty. """ paymentMethod: String! """ Code identifying the chosen payment module. Required. Must match one of the entries in the tenant's MODULE_PAYMENT_INSTALLED configuration value (a ';'-separated list of currently-installed payment modules such as 'Cod', 'NetsBank', 'VivawalletCreditcard'). Free-form values and codes from removed/renamed modules are rejected — the createOrder mutation fails loud and the error message lists the modules currently available for the connected tenant. """ paymentCode: String! """ Internal id passed to the chosen payment module's API for reconciliation. """ paymentApiId: String """ Free-form payment field captured at checkout (module-specific extra payload). """ paymentField: String """ Module-specific payment input payload as captured at checkout (JSON-encoded). """ paymentInput: JSON """ Display label for the chosen shipping method as shown to the shopper at checkout (e.g. 'Posti - Noutopiste'). Required. """ shippingMethod: String! """ Internal id passed to the chosen shipping module's API for label generation and tracking. """ shippingApiId: String """ Code identifying the chosen shipping module. Required. Must match one of the entries in the tenant's MODULE_SHIPPING_INSTALLED configuration value (a ';'-separated list such as 'Flat', 'Matkahuolto', 'Smartpost'). Free-form values are rejected. """ shippingCode: String! """ Shipping cost to charge on the order, in the order's currency. Defaults to 0. """ shippingCost: Float = 0 """ Warehouse code identifying which warehouse the order ships from when the tenant runs multi-warehouse fulfilment. Resolves against the warehouses query. """ warehouseCode: String """Line items to create on the order.""" items: [CreateOrderItemInput!] } """ Input for updateOrderStatus, the public order fulfilment update mutation. orderId identifies the order to modify and statusId is the tenant-local order status id to write to orders.orders_status. Resolve valid status ids dynamically with orderStatuses; tenants can extend the status table. trackingCode and trackingLink are optional fulfilment fields: omit a field to leave it unchanged, or pass an empty string to clear it. comments records the status-history note for this transition. """ input UpdateOrderStatusInput { """Order to update.""" orderId: Int! """ New current order status id. Must exist in orderStatuses for the connected tenant. """ statusId: Int! """ Carrier tracking code to store on the order. Omit to leave unchanged; pass an empty string to clear. """ trackingCode: String """ Carrier tracking URL to store on the order. Omit to leave unchanged; pass an empty string to clear. """ trackingLink: String """Status-history comment recorded with this update.""" comments: String } """ Localized text and SEO metadata for one Category, in one language. A Category has one CategoryDescription per (category, languageId) pair — the storefront picks the row matching the shopper's active language at render time. Carries the visible name, the slug-style URL fragment, the long descriptions shown above and below the product listing, the hero-banner copy, and the SEO meta-tags emitted into the page head. """ type CategoryDescription { """Language id this row is for (resolves against the languages query).""" languageId: Int! """ Localized category name shown in navigation, breadcrumbs, and the page header. """ name: String """ Slug-style URL fragment used in the canonical category URL (e.g. 'living-room-lamps'). """ url: String """ Full URL path the category resolves to (e.g. 'lighting/living-room-lamps'), composed by the storefront router. """ path: String """ Previous URL path before the most recent rename. Used to emit redirects from the old URL to the new one. """ pathOld: String """ Long-form description rendered below the product listing on the category page. """ description: String """ Optional intro copy rendered above the product listing on the category page (typically a one-paragraph teaser / SEO blurb). """ descriptionTop: String """Localized tag for the category page.""" metaTitle: String """Localized <meta name='description'> for the category page.""" metaDescription: String """ Localized title used in featured-category surfaces (homepage tiles, navigation highlights, etc.). """ featureTitle: String """ Localized title used in the category-type / facet listing (e.g. when the same category is rendered as a filter chip). """ typeTitle: String """Localized hero-banner copy on the category page.""" bannerText: String """Localized hero-banner CTA button label (e.g. 'Shop now').""" bannerButton: String """Hero-banner CTA destination URL when the shopper clicks the button.""" bannerUrl: String """ Redirect-on flag for path migrations (1 = the old slug should redirect to the new one; 0 = no redirect). Read by the router when the shopper hits pathOld. """ redirectStatus: Int } """ A node in the product category tree. Categories nest via parentId (a value of 0 indicates a top-level category) and stack into the storefront's main navigation, breadcrumbs, and faceted browsing. Carries the visual assets (image, banner) and the structural fields (parentId, sortOrder, position) directly; localized titles, URLs, and SEO copy are returned through the descriptions field — one entry per (category, languageId) pair so the storefront can pick the shopper's language at render time. """ type Category { """Database row id of the category.""" id: ID! """ Internal code for the category, stable across languages — used by ERP / PIM mappings. """ code: String """ Filename of the category thumbnail image, served from the tenant's category-image directory. Used in navigation tiles and category-card surfaces. """ image: String """ Filename of the category hero banner image, served from the tenant's category-image directory. Rendered above the product listing on the category page. """ banner: String """Id of the parent category, or 0 for top-level categories.""" parentId: Int """ Display sort order within the parent category. Lower values render first in navigation and listing. """ sortOrder: Int """ Position field used as a secondary ordering / placement hint (e.g. for featured-category slots). Tenant interpretation. """ position: Int """Localized name / URL / description / SEO rows, one per language.""" descriptions: [CategoryDescription] } """ A status label for orders, defined per tenant and per language. Returned in one row per (id, languageId) pair so storefronts and admin panels can render the status in the operator's chosen language. Tenants typically share a common core set of ids (1=Received, 2=Packing, 3=Shipped, etc. — see Order.status for the cross-tenant default list) and add custom statuses on top for payment-provider intermediate states or workflow-specific stages. Always resolve Order.status via this query rather than hardcoding ids. """ type OrderStatus { """ Stable numeric status id, used in Order.status and OrderStatusHistory.statusId. """ id: ID! """ Language the localized name is in (1=English, 5=Swedish, 6=Finnish, 9=Spanish, 14=Italian; tenants may extend). """ languageId: Int! """ Internal code for the status (e.g. 'received', 'shipped', 'awaiting_payment'). Null for legacy entries that pre-date the code column. """ code: String """ Localized display label shown to operators and (for some tenants) to customers in transactional emails. """ name: String! """ If non-zero, this status triggers ERP / order-management API push when set on an order. 0 = local-only status. """ apiFlag: Int! """ External-system identifier when the status is mirrored in an ERP (Lemonsoft / Netvisor / Tehden status code). """ apiId: String """ Hex color (without leading #) used to tint the status badge in admin UI. Null = default neutral color. """ colorCode: String } """ A status-transition entry on an order's timeline — one row per status change captured as the order progresses (Received → Packing → Shipped → Delivered, plus any backwards transitions for cancellations or returns). Each row records the new status id, an optional staff comment, and whether the customer was notified. Read-only on this type. To record a new transition and update the order's current status use updateOrderStatus. To append only a raw history row, use addOrdersStatusHistory, which writes to the same underlying table. """ type OrderStatusHistory { """Database row id of the status-transition entry.""" id: ID! """Order this status transition belongs to.""" orderId: Int! """ Numeric status id moved to. Resolve to a localized name via the orderStatuses query. """ statusId: Int! """ Localized status name as it was at the time of the transition (denormalised for stability when status names are later renamed). """ statusName: String """ISO timestamp of when the transition was recorded.""" dateAdded: String! """ Stored notification marker for this transition. Example values: - 0: not notified or not marked by the caller - 1: marked notified by the caller """ customerNotified: Int """ Free-form staff comment attached to the transition (e.g. 'Awaiting carrier pickup', 'Refunded — damaged on arrival'). """ comments: String } """ Write-side representation of a status-transition entry, returned by the addOrdersStatusHistory mutation. Maps to the same underlying orders_status_history row that OrderStatusHistory reads from, with a trimmed shape (no statusName lookup — the read query enriches it). """ type OrdersStatusHistoryEntry { """Database row id of the inserted transition.""" id: ID! """Order the transition belongs to.""" orderId: Int! """Numeric status id moved to.""" statusId: Int! """ISO timestamp of when the transition was recorded.""" dateAdded: String! """ Stored notification marker for this transition (0 = not notified, 1 = marked notified). """ customerNotified: Int """Staff comment attached to the transition.""" comments: String } """ Input shape for addOrdersStatusHistory. orderId + statusId are required; optional flags control the stored customer_notified marker and whether the order header is updated. """ input OrdersStatusHistoryInput { """Order to record the transition on.""" orderId: Int! """New status id (must exist in orderStatuses for the connected tenant).""" statusId: Int! """Free-form staff comment to attach to the transition.""" comments: String """ If true, records customer_notified = 1 on the history row. This mutation does not send email. Default: false. """ notifyCustomer: Boolean """ If true, also update the order header's current status (orders.orders_status) to this statusId. If false, the transition is recorded for audit but the order's current status is unchanged. Default: false. """ updateOrderStatus: Boolean } """ An audit-log entry recording a single change to an order or one of its child rows (line items, totals, status history, notifications). Captured automatically by the admin panel and selected automation flows whenever order data is modified after the order is placed — typically a staff edit, a refund, or a re-trigger. Use this for compliance audits, order-detail change logs, and the "history" view on the admin order page. The log is append-only at the application layer; combined with orderStatusHistory (status transitions only), it provides the complete order-modification trail. """ type OrderHistoryEntry { """Database row id of the audit entry.""" id: ID! """Order whose row was changed.""" orderId: Int! """ Source table the change happened in. Example values seen in production: - 'orders': field on the order header - 'orders_products': field on a line item - 'orders_total': totals breakdown adjustment - 'orders_status_history': status-history modification - 'orders_notifications': notification record - 'orders_products_variants': variant selection on a line - 'invoice_book', 'orders_actions': tenant-specific extensions """ entityType: String! """ Row id within the entity table when the change targets a specific row (line id, totals row id, etc.). Null for header-level changes on orders itself. """ entityId: Int """ Database column name that changed (e.g. 'orders_status', 'products_quantity', 'shipping_method'). """ columnName: String! """ Previous value as stored in the column, serialized to string. Null when the row was newly created or the field was previously null. """ oldValue: String """ New value as stored in the column, serialized to string. Null when the field was cleared. """ newValue: String """ Kind of change. Example values seen in production: - 'update' (most common): a column on an existing row was modified - 'rebuild': bulk recomputation (e.g. totals re-derived from lines) - 'delete': row was deleted - 'insert': row was created - 'notify': a notification was emitted to the customer - 'action': tenant-specific custom action """ actionType: String! """ Administrator user id who made the change. Null for automated / system-triggered changes. """ userId: Int """Administrator user name (denormalised at write time for stability).""" userName: String """ Admin panel route the change was made from (helps trace whether it came from a list page, detail page, or bulk action). """ page: String """ISO timestamp of when the change was recorded.""" createdAt: String! } """ Storefront-side i18n metadata for one language. The image and directory fields come from the legacy oscommerce-derived language bundle layout — image holds the flag-icon filename rendered in the language switcher, while directory names the bundle folder containing the language's PHP translation files (e.g. 'finnish', 'english'). """ type LanguageMetadata { """ Filename of the flag icon used in the language switcher, served from the tenant's flag-icon directory. """ image: String """ Name of the language-bundle directory (oscommerce-style i18n folder, e.g. 'finnish', 'english'). Used by the legacy admin to load the localized PHP translations. """ directory: String } """ A language enabled for the tenant. Drives the languages dropdown on the storefront / admin, every per-language description row across the schema (CategoryDescription, FaqQuestionDescription, OrderStatus, etc.), and the localized labels emitted in transactional emails and receipts. The platform's core canonical ids — 1=English, 5=Swedish, 6=Finnish, 9=Spanish, 14=Italian — are shared across tenants; tenants may add more by inserting rows here and creating matching *_description rows for each localized table. Every typedef in the schema that references a languageId resolves it against this table. Always resolve dynamically rather than hardcoding — the canonical id list isn't enforced at the DB level and tenants can extend it. """ type Language { """ Database row id of the language (language_id). Used as the languageId in every per-language description row across the schema. """ id: ID! """ ISO 639-1 short code (e.g. 'fi', 'sv', 'en'). The code shoppers see in the URL when language is encoded in the path. """ code: String! """ Display name of the language in the language's own form (e.g. 'Suomi' for Finnish). """ name: String! """ Storefront i18n metadata: flag icon filename and language-bundle directory. """ metadata: LanguageMetadata } """ A stock-availability label shown on product detail and listing pages (e.g. "In Stock", "Out of Stock", custom delivery-window phrases). Each tenant defines its own set of labels. Ids 1 and 2 are conventionally reserved for "In Stock" and "Out of Stock"; ids 3+ are tenant-defined delivery-window variants ("usually ships in 1–3 days", "special order", etc.). Returned in one row per (id, languageId) pair so storefronts can pick the shopper's language at render time. """ type Availability { """Stable numeric id of the availability label across all languages.""" id: ID! """ Language identifier matching the languages query. Example values: - 1: English - 5: Swedish - 6: Finnish - 9: Spanish - 14: Italian Tenants may add or remove languages — resolve dynamically via the languages query rather than hardcoding ids. """ languageId: Int! """Localized label shown to shoppers in the chosen language.""" name: String! """ Hex color (without leading #) used to tint the label in storefront UI. Common conventions: - '005900' (dark green): item is available - 'ff0000' (red): item is unavailable or has an extended delivery window May be null for legacy labels that pre-date the colored-badge UI. """ hexCode: String } """ A physical (or 3PL / virtual) warehouse the tenant ships from. Drives multi-warehouse fulfilment routing (which warehouse picks an order), per-warehouse stock split (WarehouseStock holds the per-warehouse on-hand quantities), and the storefront's 'check stock at your local store' surface. A tenant on single-warehouse fulfilment will typically have one row here (often code 'MAIN'); multi-warehouse setups use the visible flag to control which warehouses appear in shopper-facing surfaces vs. back-office-only locations. Each warehouse exposes a paginated stock view via Warehouse.stock — same data as the warehouseStock query reachable via this side of the join. """ type Warehouse { """Database row id of the warehouse.""" id: ID! """Display name of the warehouse (e.g. 'Helsinki Main', 'Stockholm Hub').""" name: String """ Internal short code (e.g. 'MAIN', 'WH1', 'SE-HUB'). Used by Order.warehouseCode and as the join key into ERP / WMS systems. """ code: String """ Contact phone for the warehouse — captured for storefront display on the 'Find a store' surface and for B2B contact flows. """ phone: String """Contact email for the warehouse.""" email: String """ Whether the warehouse should appear in shopper-facing surfaces (the 'check stock' picker, store-locator). false = back-office-only (e.g. a 3PL hub the storefront shouldn't expose by name). """ visible: Boolean! """ Per-warehouse stock for one product (or every product when productId is omitted). Resolves the warehouseStock data filtered to this warehouse — same rows the warehouseStock query returns when called with a warehouseId filter. """ stock( """ Restrict the response to one product. Omit to receive every product's quantity at this warehouse. """ productId: Int ): [WarehouseStock] } """ Per-warehouse on-hand quantity for one product. When a tenant runs multi-warehouse fulfilment (e.g. a main warehouse plus regional satellites or a 3PL), the product's total inventory in ProductStock is split across warehouses here — one row per (warehouse, product) pair, holding only that warehouse's slice. ProductStock holds the variant-level totals across all warehouses; WarehouseStock holds the warehouse-level split per product. Sum WarehouseStock.quantity for a product across warehouses to reconcile against ProductStock.quantity. Used by the picking / dispatch flow to pick the right warehouse, by the storefront's 'in stock at your nearest store' display, and by ERP / WMS reconciliation reports. """ type WarehouseStock { """Database row id of the per-warehouse stock entry.""" id: ID! """Warehouse this stock slice lives in.""" warehouseId: Int! """Product this stock slice is for.""" productId: Int! """On-hand quantity at this warehouse for this product.""" quantity: Int! } """ Localized text and click-tracking metadata for one Manufacturer, in one language. Carries the long brand body and the intro / preamble copy that render on the brand landing page, plus a counter of how many times the brand's external URL has been clicked from the storefront and the timestamp of the most recent click. """ type ManufacturerInfo { """Language id this row is for (resolves against the languages query).""" languageId: Int! """ Counter of clicks on the brand's external URL from the storefront. Used for brand-engagement reporting. """ urlClicked: Int """ Timestamp of the most recent external-URL click. Null when the brand's URL has never been clicked. """ dateLastClick: String """ Long-form brand description body (rich text). Rendered below the intro on the brand landing page. """ description: String """ Intro / preamble copy rendered above the long description on the brand landing page (typically a one-paragraph teaser / SEO blurb). """ descriptionTop: String } """ A brand or manufacturer associated with one or more products in the catalog. Drives the storefront's brand index, the brand-filter facet on category pages, the per-brand landing page, and any brand-scoped pricing rules (special discounts applied at the brand level). Carries three image slots for different rendering surfaces (hero, brand-page banner, listing thumbnail), per-brand commerce flags (status visibility, homepage feature, country-of-sale scope), and one ManufacturerInfo row per language for the localized body copy. """ type Manufacturer { """Database row id of the manufacturer (manufacturers_id).""" id: ID! """ Brand display name shown in navigation, breadcrumbs, and the brand-page header. """ name: String """ Internal code or short identifier for the brand. Stable across languages — used by ERP / PIM mappings and for routing rules. """ code: String """ Filename of the brand hero image (the large brand logo / hero shown on the brand landing page). Served from the tenant's manufacturer-image directory. """ image: String """ Filename of the brand-page banner image — wider hero / banner slot rendered above the product listing on the brand landing page. """ brandPageImage: String """ Filename of the listing thumbnail used in search results, brand-grid tiles, and category-side brand chips. """ listingImage: String """Display sort order across the brand index. Lower values render first.""" sortOrder: Int """ Visibility flag. 1 = brand is active and visible on the storefront; 0 = hidden (kept for editing without going live). """ status: Int """Timestamp the brand record was created.""" dateAdded: String """Timestamp of the most recent edit to the brand record.""" lastModified: String """ Brand-specific size / fit hint shown on product detail pages (e.g. 'runs small', 'true to size'). Free-form short text. """ fitValue: String """ Brand-level percentage discount applied across the brand's products at checkout (a tinyint flag / value). 0 means no brand-level discount. """ specialDiscount: Int """ Flag indicating the description is sourced from a CMS instead of the inline ManufacturerInfo body. 1 = use CMS, 0 = use inline. """ cmsDescription: Int """ Comma-separated country codes where the brand is approved for sale. Empty / null means no country restriction. """ countryApprovedProduct: String """ Whether the brand is featured on the storefront homepage. 1 = featured (rendered in the homepage brand carousel / spotlight), 0 = not featured. """ displayFrontpage: Int """Localized description / click-tracking rows, one per language.""" infos: [ManufacturerInfo] } """ A line item on a quote — references a product, an optional variant, quantity, and any per-line pricing overrides. Mirrors the shape of an OrderItem but for the pre-order quote flow: prices may be tentative (discountedPrice override), variant selections may still be in flux, and the line carries a productGroup marker for combined-discount rules across grouped lines. Captured per quote — when a quote is later converted to an order, the resolver translates these QuoteProduct rows into OrderItem + OrderItemVariant rows on the new order. """ type QuoteProduct { """Database row id of the quote line.""" id: ID! """Quote this line belongs to.""" quoteId: ID! """Product id from the catalog.""" productId: Int! """ Variant identifier — typically a comma-separated list of option-value ids (matches the format used in ProductStock.variant). Empty string when the product has no variants. """ variant: String! """Quantity quoted on this line.""" quantity: Int! """ Per-line negotiated price override. Null means use the product's catalog price. """ discountedPrice: Float """ Bundle / pricing-rule grouping marker. 0 means ungrouped (default for most lines). Non-zero values group multiple lines together so combined-discount rules and kit-style packaging can be evaluated across the group as a unit. """ productGroup: Int """ Free-form per-line comment from the customer or sales rep (configuration notes, customisation requests). """ comment: String """ JSON-encoded free-text custom-option inputs (the counterpart to product-options choices, e.g. engraving text, monogram initials). """ customOptions: String """ Bundle parent id when the line is a child of a configured product bundle. 0 means a standalone line. """ bundleId: Int } """ A quote — a B2B request-for-pricing record created by a customer or on their behalf by a sales rep. Quotes progress through their own status lifecycle (Open → Proposal sent → Accepted → Ordered, or Cancelled / Expired, see quoteStatuses), can be tied to a customer account or held under an anonymous sessionId, and aggregate one or more QuoteProduct line items. Carries the negotiation state (statusId, dates, reminder / follow-up cadence flags), the order-level discount applied to the quote (discountType + discountAmount), preferred shipping (shipmentMethod / shipmentCode / shipmentPrice), preferred warehouse (warehouseId for stock pre-allocation), and the cc field for follow-up email recipients. Once a customer accepts a quote, the quote-to-order conversion flow spawns a new Order with the QuoteProduct lines mapped onto OrderItem rows. For attachments see quoteFiles; for the status timeline see quoteStatusHistory; for status definitions see quoteStatuses. """ type Quote { """Database row id of the quote.""" id: ID! """ Id of the requesting customer. Null for anonymous quotes (filled in via sessionId until the requester logs in / registers). """ customerId: Int """ Anonymous browser session identifier when the quote is created without a customer account. Lets the storefront thread the quote across page views before login. """ sessionId: String """ IP address of the requester captured at quote creation. Useful for fraud / abuse forensics. """ ipAddress: String """ Numeric status id of the quote. Example values across tenants (defaults; tenants extend): - 1: Open (received) - 2: Accepted - 3: Cancelled - 4: Ordered (converted to an order) - 5: Proposal sent - 50: Pending approval Tenants may extend this set. Always resolve dynamically via the quoteStatuses query. """ statusId: Int """Timestamp the quote was created.""" dateAdded: String """Timestamp of the most recent edit to the quote.""" dateModified: String """ Timestamp the auto-reminder system is scheduled to dispatch a reminder email at. Paired with sendReminder. """ dateReminder: String """ Timestamp the auto-reminder system is scheduled to dispatch a follow-up email at. Paired with sendFollowUp. """ dateFollowUp: String """ Whether the auto-reminder system should send the dateReminder email. Stored as a flag string (e.g. '1' / '0'). """ sendReminder: String """ Whether the auto-reminder system should send the dateFollowUp email. Stored as a flag string (e.g. '1' / '0'). """ sendFollowUp: String """ How the discountAmount is interpreted. Same enum as on coupons and customer groups. Example values: - 'fixed' — currency amount off the quote total - 'percent' — percentage off - 'shipping' — applied as a shipping subsidy / free shipping Defaults to 'fixed' on new quotes. """ discountType: String """ Discount value to apply at the quote level, interpreted via discountType. """ discountAmount: Float """ Free-form comment captured at quote creation (customer's brief or sales-rep notes). """ comment: String """ Comma-separated list of CC email addresses to copy on quote-related notifications (proposal email, reminders, follow-ups). """ cc: String """ Display label for the requested shipping method (e.g. 'Posti - Noutopiste'). Mirrors the shape on Order.shippingMethod. """ shipmentMethod: String """ Code identifying the requested shipping module. Mirrors Order.shippingCode. """ shipmentCode: String """Quoted shipping cost in the tenant's default currency.""" shipmentPrice: Float """ Preferred warehouse id for stock pre-allocation when the quote is later converted to an order. Resolves against the warehouses query. """ warehouseId: Int """Line items on the quote.""" items: [QuoteProduct!]! } input QuoteProductInput { """Product id from the catalog.""" productId: Int! """ Variant identifier — comma-separated option-value ids (matches ProductStock.variant). Empty string when the product has no variants. """ variant: String! """Quantity to quote. Defaults to 1.""" quantity: Int = 1 """Per-line negotiated price override. Null = use the catalog price.""" discountedPrice: Float """Bundle / pricing-rule grouping marker. Defaults to 0 (ungrouped).""" productGroup: Int = 0 """Free-form per-line comment.""" comment: String = "" """JSON-encoded custom-option inputs.""" customOptions: String """Bundle parent id. Defaults to 0 (standalone line).""" bundleId: Int = 0 } input CreateQuoteInput { """ Requesting customer id. Optional — leave null for anonymous quotes captured via sessionId. """ customerId: Int """Anonymous browser session id when no customer is logged in.""" sessionId: String """Requester IP address (forensics).""" ipAddress: String """ Initial status id. Defaults to the tenant's 'Open' status when omitted. """ statusId: Int """Display label for the requested shipping method.""" shipmentMethod: String """Code identifying the requested shipping module.""" shipmentCode: String """Quoted shipping cost.""" shipmentPrice: Float """Preferred warehouse id for stock pre-allocation.""" warehouseId: Int """Line items to create on the quote. Required (at least one).""" items: [QuoteProductInput!]! } """ A localized quote-status label. Mirrors the order-status pattern but for B2B quotes — quotes move through their own lifecycle (Open → Proposal sent → Accepted → Ordered, or Cancelled / Expired) and the storefront / admin renders the current status using the localized name returned here. The platform's canonical status ids — 1 Open, 2 Accepted, 3 Cancelled, 4 Ordered, 5 Proposal sent, 50 Pending approval — are shared across tenants by default; tenants can extend with custom workflow stages. Each status has a stable internal code (e.g. 'open', 'accepted') in addition to the localized display name. """ type QuoteStatus { """ Stable numeric status id, used in Quote.statusId and QuoteStatusEntry.statusId. """ id: ID! """Language id this row is for.""" languageId: Int! """ Localized display label shown to operators and customers (e.g. 'Open', 'Hyväksytty', 'Avbruten'). """ name: String! """ Stable internal code for the status, language-independent (e.g. 'open', 'accepted', 'cancelled', 'ordered'). Use this for routing rules and integrations rather than matching on numeric id or localized name. """ code: String! } """ A status-transition entry on a quote's timeline — one row per status change captured as the quote progresses. Mirrors OrderStatusHistory but for quotes. Read-only on this type; record new transitions via the addQuoteStatusHistory mutation, which writes to the same underlying table. """ type QuoteStatusEntry { """Database row id of the timeline entry.""" id: ID! """Quote this transition belongs to.""" quoteId: Int! """ Numeric status id moved to. Resolve to a localized name via the quoteStatuses query. """ statusId: Int! """ Localized status name as it was at the time of the transition (denormalised for stability when status names are later renamed). """ statusName: String """ISO timestamp of when the transition was recorded.""" dateAdded: String! """ Whether a notification email was sent to the customer for this transition. Example values: - 0: not notified (internal state change) - 1: notification email sent """ customerNotified: Int """ Free-form operator comment attached to the transition (e.g. 'Awaiting customer signature'). """ comments: String } input AddQuoteStatusHistoryInput { """Quote to record the status transition against.""" quoteId: Int! """ Numeric status id to move the quote to. Resolves against quoteStatuses. """ statusId: Int! """ Whether to flag the entry as having dispatched a customer notification (0 = not notified, 1 = notified). """ customerNotified: Int """ Free-form operator comment attached to the transition (e.g. 'Awaiting customer signature'). """ comments: String } """ Optional notification-cadence flags applied alongside a status transition. The reminder / follow-up flags on the parent Quote control whether the auto-reminder system will email the customer if the quote sits in its current status — recording a transition is a natural moment to flip those flags (e.g. enable follow-up emails when the quote moves to 'Proposal sent'). """ input QuoteNotificationFlagsInput { """ When set, update Quote.sendReminder. true = enable the reminder cadence; false = disable it. """ sendReminder: Boolean """ When set, update Quote.sendFollowUp. true = enable the follow-up cadence; false = disable it. """ sendFollowUp: Boolean } """ An attachment on a quote. Either a customer-supplied document (specification, drawing, brief) uploaded when the quote is requested, or a seller-generated PDF (the proposal sent back to the customer) attached as the quote progresses. Stored as a filename pointer — the actual file lives in the tenant's quote-files directory. """ type QuoteFile { """Database row id of the attachment.""" id: ID! """Quote this file is attached to.""" quoteId: Int! """ Filename of the attachment, served from the tenant's quote-files directory. """ file: String! } input QuoteFileInput { """Quote id to attach the file to.""" quoteId: ID! """ Filename to record. The actual upload happens through the tenant's file-upload endpoint; this mutation just records the name. """ file: String! } """ A localized quote-group label — the categorisation a customer or operator picks when creating a quote (e.g. 'Wholesale', 'B2B project', 'Sample request', 'Construction tender'). Each group has one row per language, so the storefront's quote form can render the group dropdown in the shopper's chosen language. Tenants define their own group set; the values are not enforced as an enum at the GraphQL layer. Used for routing — quotes in some groups go to specific sales reps or trigger different reminder cadences than others. """ type QuoteGroup { """Database row id of the quote group.""" id: ID! """Language id this row is for (resolves against the languages query).""" languageId: Int! """Localized group label shown in the quote-group dropdown.""" name: String! } """ A configurable option attached to a product — for example "Size" with values XL/L/M/S, "Engraving" with a free-text value, or "Material" with a price- affecting choice. ProductOption represents the option itself; the ProductOptionValue list holds the choices the shopper can pick. Options drive the variant selector on product detail pages and seed OrderItemVariant rows when the shopper checks out. The same configurable- option model also feeds quotes (QuoteProduct.variant) and per-line customisations on saved baskets. """ type ProductOption { """Database row id of the option (products_options_id).""" id: ID! """ Localized display label of the option (e.g. 'Size', 'Material', 'Engraving'). """ name: String """ Internal code for the option, stable across languages — useful for routing rules and ERP mappings. """ code: String """Localized helper text shown beneath the option in the product page UI.""" description: String """Display sort order. Lower values render first in the variant selector.""" sort: Int """ The selectable values for this option. Empty for free-form options where the shopper types a value at checkout. """ values: [ProductOptionValue] } """ One selectable value of a ProductOption — for example 'XL' under 'Size', or 'Engraving up to 12 chars' under 'Personalisation'. Carries an optional price delta that's applied at the line level when the shopper picks this value (see also OrderItemVariant.price + pricePrefix for the persisted form). """ type ProductOptionValue { """Database row id of the option value (products_options_values_id).""" id: ID! """Localized label shown to shoppers (e.g. 'XL', 'Stainless steel').""" name: String """Internal code for the value, stable across languages.""" code: String """ Free-form spec text shown alongside the value (e.g. dimensions, technical detail). """ specification: String """ Weight delta added to the product's base weight when this value is chosen, in kilograms. Used in shipping calculation. """ weight: Float """Display sort order within the parent option.""" sort: Int """ Hex color (without leading #) for swatch UI. Null when the value isn't a color. """ hex: String """ Price delta this value contributes when chosen, in the tenant's default currency. Interpreted via pricePrefix. """ priceDelta: Float """ How priceDelta is applied. Example values: - '+' (most common): additive — added to the base product price - '' (empty): replacement — overrides the base price entirely Mirrors OrderItemVariant.pricePrefix for the persisted form. """ pricePrefix: String """ External-system identifier for the value when the option is mirrored in an ERP / PIM. """ apiId: String } """ A stock-keeping row for one variant of a product. Products that come in multiple variants (size, color, configuration) have one ProductStock row per variant, each with its own SKU, barcode, price, on-hand quantity, and warehouse shelf location. ProductStock holds the totals across all warehouses for a given variant. When a tenant runs multi-warehouse inventory, the per-warehouse split lives in WarehouseStock — sum WarehouseStock.quantity across warehouses for one product to reconcile against ProductStock.quantity. Price changes are audited via productPriceHistory. """ type ProductStock { """Database row id of the stock entry (products_stock_id).""" id: ID! """Product this variant belongs to.""" productId: ID! """ Variant identifier — typically a comma-separated list of option-value ids that uniquely identify the variant within the product (e.g. '11_22' = option 11 value 22). """ variant: String """On-hand quantity for this variant, summed across all warehouses.""" quantity: Int! """ Current selling price for this variant in the tenant's default currency. """ price: Float! """ Reference / list price before any current promotion. Used to compute discount badges in the storefront. """ priceOriginal: Float! """ Warehouse shelf location code captured for pick-and-pack staff (e.g. 'A1-03'). Null when the tenant doesn't track shelves. """ shelf: String } """ A snapshot of one price change for a product. Every time a product's price is updated — at the product level or for a specific variant — a row is written here capturing the new price, the timestamp, and the user who made the change. The table is append-only and serves as the audit trail behind the admin's pricing reports and any 'price last changed by …' display. For the live current price see ProductStock.price (per-variant) and the product-level price on Product. For volume / step-discount ladders that layer on top of the live price see productVolumeAddonPrices and productVolumeDiscounts. """ type ProductPriceHistory { """Database row id of the history entry.""" id: ID! """Product whose price changed.""" productId: ID! """ Variant (products_stock) whose price changed. 0 means a product-level price change rather than a per-variant change. """ stockId: ID! """New price recorded by this change, in the tenant's default currency.""" price: Float! """Timestamp of the price change. ISO-8601 string.""" date: String! """ Username of the admin who applied the change. Useful for audit / accountability reports. """ userName: String! } """ A many-to-many link row tying one product to one category. The catalog navigation, breadcrumbs, and category landing pages all rely on this pivot table — a product lives in multiple categories at once (a lamp can be in 'Living room', 'Floor lamps', and 'Sale'), and each category lists every product linked to it. This is a thin pivot — id-only — but the resolver hydrates the related Product and Category objects when included in the selection set, so the caller can fetch link rows + their endpoints in a single round-trip. """ type ProductCategoryLink { """Product side of the link.""" productId: ID! """Category side of the link.""" categoryId: ID! """Resolved product, included when the field is selected.""" product: Product """Resolved category, included when the field is selected.""" category: Category } """ One line item in a customer's persistent (server-side) basket. Unlike the volatile session basket, customers_basket rows survive logout and device switch — they're the storefront's 'we saved your cart' surface, the basket the abandoned-basket reminder emails are built on, and the starting point when a logged-in customer adds the same product again. A line carries the product, the chosen variant + option values (variants / productOptions), any free-form custom-option text inputs (customOptions), and an explicit price snapshot (specialPrice) when the storefront wants to lock in a promotional price. Bundle support is via bundleId + includes for parent / child line linking. """ type CustomersBasketItem { """Database row id of the basket line (customers_basket_id).""" id: ID! """ Cart key. Lets one customer hold multiple parallel baskets (saved-for-later, B2B quote draft, etc.) keyed by this opaque id. """ cartId: String! """Customer id this basket line belongs to.""" customersId: Int! """ Free-form item identifier captured at add-to-cart time. Used by some integrations to match the line back to an external system. """ itemId: String """ Product id added to the basket. Null for bundle child rows that point at the parent via bundleId instead. """ productId: Int """ Variant identifier for the line — matches the comma-separated option-value-id format used in ProductStock.variant (e.g. '11_22' = option 11 value 22). Null when the product has no variants. """ variants: String """ Configurable-option choices captured at add-to-cart, encoded the same way OrderItemVariant rows persist them on a finalized order. """ productOptions: String """ Free-form line comment from the shopper (e.g. gift-message, delivery instructions for this line specifically). """ comment: String """ Custom-option text inputs captured at add-to-cart (the free-text counterpart to productOptions, e.g. engraving text, monogram initials). """ customOptions: String """Quantity ordered on this line.""" quantity: Int! """ Price snapshot to lock the line to, in the tenant's default currency. Null = use the live product price at checkout. Set when the storefront wants to honour a promotional price even if the catalog price changes before checkout. """ specialPrice: Float """ Bundle parent id for child rows. Null = this line is either a standalone product or the bundle parent itself. """ bundleId: String """ Bundle-children references when this row is a bundle parent (free-form encoding identifying the included products / lines). """ includes: String """ Timestamp the line was added to the basket. Used to age out stale baskets. """ dateAdded: String """ Abandoned-basket reminder counter. Tracks how many reminder emails the line has triggered so far. """ emailSent: Int """ Timestamp of the most recent abandoned-basket reminder email sent for the line. """ emailLast: String """ Variant-specific image filename to render on the basket page (e.g. when the customer picked a colour variant). """ variableImage: String """ Whether the customer is OK with the warehouse shipping a substitute item if this product is out of stock at pick time. Captured for B2B / wholesale flows where missing stock can be swapped for an equivalent. """ allowSubstitute: Boolean! } """ One line item in a shared basket — a basket bound to an opaque shared_cart_id rather than a specific customer's session. Used for collaborative B2B flows: a sales rep builds a basket on behalf of a customer and sends them a link, two team-mates from the same customer organisation co-edit a wholesale draft, or a single shopper syncs their cart across devices via a shared id. Same shape as CustomersBasketItem but keyed differently: customersId is optional (null on anonymous shared baskets), and the row carries an explicit expiresAt so the storefront can age out abandoned shared baskets. Each item is uniquely keyed by (sharedCartId, itemId) — the itemId lets the parent flow address one line of the shared basket without knowing the row id. """ type SharedBasketItem { """Database row id of the shared-basket line.""" id: ID! """ Opaque shared-cart id linking lines that belong to the same shared basket. Lets multiple parties co-edit one basket without a shared customer id. """ sharedCartId: String! """ Customer attached to the basket. Null for anonymous shared baskets that haven't been claimed by a logged-in customer yet. """ customersId: Int """ Caller-supplied identifier for the line within the shared basket. Lets the parent flow reference a specific line ('apply discount to itemId X') without knowing its database row id. """ itemId: String """ Catalog product id on the line. Null for free-form / placeholder lines. """ productId: Int """ Variant identifier — comma-separated option-value ids matching the format used in ProductStock.variant. Empty when the product has no variants. """ variants: String """ Configurable-option choices (matches OrderItemVariant / ProductOption choices, encoded the same way as on customers_basket). """ productOptions: String """Free-form per-line comment (configuration notes, gift message).""" comment: String """JSON-encoded free-text custom-option inputs (engraving, monogram).""" customOptions: String """Quantity ordered on this line.""" quantity: Int! """ Price snapshot to lock the line to, in the tenant's default currency. Null = use the live product price at checkout. """ specialPrice: Float """ Bundle parent id for child rows. Null = standalone line or bundle parent itself. """ bundleId: String """Bundle-children references when this row is a bundle parent.""" includes: String """Timestamp the line was added to the shared basket.""" dateAdded: String """ Timestamp the shared-basket line auto-expires. Past this point the storefront should refuse to load or accept edits unless requireActive=false is passed on the mutation. """ expiresAt: String } """ Input for createOrUpdateSharedBasket — upsert a line into a shared basket. When a row with the same (sharedCartId, itemId) exists, the resolver updates it; otherwise it inserts a new row. requireActive guards against edits on already-expired baskets. """ input CreateSharedBasketInput { """Opaque shared-cart id.""" sharedCartId: String! """Caller-supplied line identifier within the shared basket.""" itemId: String! """ Customer to bind the line to. Optional — leave null for anonymous shared baskets. """ customersId: Int """Catalog product id.""" productId: Int """Variant identifier (comma-separated option-value ids).""" variants: String """Configurable-option choices.""" productOptions: String """Free-form per-line comment.""" comment: String """JSON-encoded custom-option text inputs.""" customOptions: String """Quantity to set on the line.""" quantity: Int! """Price snapshot to lock the line to.""" specialPrice: Float """Bundle parent id for child rows.""" bundleId: String """Bundle-children references when this row is a bundle parent.""" includes: String """Timestamp the line should expire at.""" expiresAt: String """ When true (default), the resolver throws if the existing line is past expiresAt. Set false to permit edits on expired baskets (e.g. an admin reviving a stale shared basket). """ requireActive: Boolean } """ Input for updateSharedBasketQuantity — narrowly change the quantity on an existing line without touching the rest of the row. Used by the storefront's '+' / '-' quantity stepper on the shared-basket view. """ input UpdateSharedBasketQuantityInput { """Opaque shared-cart id.""" sharedCartId: String! """Caller-supplied line identifier.""" itemId: String! """New quantity for the line.""" quantity: Int! """When true (default), refuse to update if the line is past expiresAt.""" requireActive: Boolean } """ Input for expireSharedBasketItem — rewrite the line's expiresAt to the current moment (or the supplied timestamp) so subsequent requireActive=true reads / edits start refusing. Used by 'Mark as expired' admin actions and by the auto-expiry sweeper. """ input ExpireSharedBasketInput { """Opaque shared-cart id.""" sharedCartId: String! """Caller-supplied line identifier.""" itemId: String! """ Timestamp to set as the new expiresAt. When omitted the resolver uses 'now'. """ expiresAt: String } """ A discount coupon definition — the rule that says 'enter code XYZ at checkout to get N off, applicable when …'. Coupons can be unrestricted (apply to any basket) or scoped to specific products, categories, customers, manufacturers, shipping methods, or geo-zones via the applicability arrays (categories, products, customers, manufacturers, shippingMethods, zones) — each populated from a dedicated discount_coupons_to_* join table. The orders[] array on this type doubles as the redemption history — it contains the order ids the coupon has actually been applied to. For per-order lookup of which coupons were applied see discountCouponsToOrders / Order.discountCoupons; for per-customer attach/revoke flows see attachDiscountCouponToCustomer / removeDiscountCouponFromCustomer mutations. """ type DiscountCoupon { """ Coupon id (a row in the discount_coupons table). Strings on the wire — most tenants use human-readable codes like 'WELCOME10' rather than numeric ids. """ id: ID! """ Admin-facing description (e.g. 'Welcome 10% off'). Not shown to shoppers. """ description: String! """Discount value, interpreted via discountType.""" amount: Float! """ How amount is applied. Example values: - 'fixed' — fixed currency amount off the order total - 'percent' — percentage off the order total - 'shipping' — applied as a shipping subsidy / free shipping """ discountType: String! """ Inclusive lower bound on when the coupon is redeemable. Null = no lower bound. """ dateStart: String """ Inclusive upper bound on when the coupon is redeemable. Null = no upper bound. """ dateEnd: String """Maximum total redemptions across all customers. 0 = unlimited.""" maxUse: Int! """ Minimum order threshold for the coupon to apply, interpreted via minOrderType. """ minOrder: Float! """ How minOrder is interpreted. Example values: - 'price' — basket value in the tenant's default currency - 'quantity' — total item count in the basket """ minOrderType: String """ Remaining redemptions allowed across all customers. Decremented each time the coupon is applied to an order; reaches 0 when the coupon is exhausted (subject to maxUse). 0 / null when unbounded. """ numberAvailable: Int! """ Customer-eligibility filter. Example values: - 'all' — any customer can redeem - 'vatid' — only customers with a VAT id (B2B accounts) - 'regular' — only customers without a VAT id (consumer accounts) """ customerType: String! """ Cached count of products the coupon currently applies to (rolled up from the products / categories / manufacturers join tables). Convenience for admin UI; recomputed when applicability changes. """ discountedProducts: Int """ Cached count of products the coupon previously applied to before the most recent applicability edit. Used by the admin to show 'previous vs current' diffs when editing a coupon. """ discountedProductsOld: Int """ Free-form whitelist of additional product ids (typically comma-separated) the coupon applies to even if the categories / products join tables don't list them. Null when no extra whitelist is set. """ allowedProducts: String """ Category ids the coupon is restricted to (from discount_coupons_to_categories). Empty array means 'no category restriction'. """ categories: [Int!] """ Product ids the coupon is restricted to (from discount_coupons_to_products). Empty array means 'no per-product restriction'. """ products: [Int!] """ Customer ids the coupon is restricted to (from discount_coupons_to_customers) — the per-customer issued-coupon allowlist. Empty array means 'any eligible customer can use it'. """ customers: [Int!] """ Order ids the coupon has been redeemed against (from discount_coupons_to_orders). Doubles as the redemption history. """ orders: [Int!] """ Manufacturer ids the coupon is restricted to (from discount_coupons_to_manufacturers). Empty array means 'no manufacturer restriction'. """ manufacturers: [Int!] """ Shipping-method codes the coupon is restricted to (from discount_coupons_to_shipping). Empty array means 'no shipping-method restriction'. """ shippingMethods: [String!] """ Geo-zone ids the coupon is restricted to (from discount_coupons_to_zones). Empty array means 'no geo restriction'. """ zones: [Int!] } """ One redemption-history entry — a row in discount_coupons_to_orders recording that a specific coupon was applied to a specific order at checkout. Same join table that's exposed as DiscountCoupon.orders[] (the from-coupon side) and as Order.discountCoupons (the from-order side); this surface is the standalone link-row view, useful for cross-cutting queries that want the join with both endpoints resolvable in one round-trip. The link is created at checkout when the customer applies a coupon successfully, and is read at order display, refund / return flows, and coupon-performance reporting. Removing a coupon from an order after the fact requires deleting this link row plus reversing the numberAvailable counter on the parent DiscountCoupon — there's no built-in mutation for that here. """ type DiscountCouponOrderLink { """Coupon id that was applied (matches DiscountCoupon.id).""" couponCode: ID! """Order id the coupon was applied to.""" orderId: ID! """ Resolved coupon, fetched on demand when the field is selected. Returns the full DiscountCoupon with its applicability arrays. """ coupon: DiscountCoupon """ Resolved order, fetched on demand when the field is selected. Returns a slim Order shape (customer, status, totals, payment / shipping method). """ order: Order } """ A per-customer issuance link — one row in discount_coupons_to_customers binding one coupon to one customer. When a coupon's customers list is non-empty, only the listed customers can redeem it (private / VIP / named-account coupons). Empty list = anyone eligible by the coupon's other rules can redeem. Same join surface that's exposed as DiscountCoupon.customers[] (the read view from the coupon side). Use the mutations here to attach a coupon to a customer (idempotent via ON DUPLICATE KEY) or detach it. """ type DiscountCouponCustomerLink { """Coupon code (matches DiscountCoupon.id).""" couponCode: ID! """Customer the coupon is issued to (matches Customer.id).""" customerId: ID! } input DiscountCouponCustomerLinkInput { """Coupon code to attach / detach.""" couponCode: ID! """Customer id to attach the coupon to or detach it from.""" customerId: ID! } """ A configurable-product option selected on one order line — for example a size/colour pair on a t-shirt, or a personalisation field on a gift. Variants stack: a single OrderItem can carry many OrderItemVariants, each contributing an additive or replacement price delta to the line total via pricePrefix. Captured as a snapshot at order time, so later changes to the underlying product option definition do not retroactively rewrite already-placed orders. """ type OrderItemVariant { """Database row id of the variant selection.""" id: ID! """Order line (OrderItem) this variant selection belongs to.""" orderProductId: Int! """Product option id (matches a row in the catalog options table).""" optionId: Int! """ Display label of the option as shown to the shopper at checkout (e.g. 'Size', 'Engraving'). """ optionName: String! """ Selected value as shown to the shopper (e.g. 'XL', 'Happy Birthday Anna'). """ optionValue: String! """ Price delta this variant contributes to the line, interpreted via pricePrefix. """ price: Float! """ How the price delta is applied to the line: - '+' (most common): additive — added to the base product price - '' (empty): replacement — overrides the base price entirely Tenants may define additional prefixes; treat unrecognised values as additive by default. """ pricePrefix: String } """ A purchased line on an order — the snapshot of one product (or shipping / fee / comment line) as it appeared in the cart at the moment of checkout. Includes the per-line price, tax, configurable variant selections, and warehouse pick-and-pack state that gets populated as the order moves through fulfilment. Read-only on this type. To modify lines on an existing order use the addOrdersProduct / updateOrdersProduct / removeOrdersProduct mutations, which write to the same underlying orders_products row but expose a write-shaped contract via the OrdersProduct type. """ type OrderItem { """Database row id of the order line (orders_products_id).""" id: ID! """Order this line belongs to.""" orderId: Int! """ Product id from the catalog. Null for lines that don't reference a catalog product (free-form refund / fee lines). """ productId: Int """Product SKU / model identifier captured at order time.""" productModel: String """EAN / barcode captured at order time.""" productEan: String """Display name shown on the receipt and admin order detail.""" productName: String! """Quantity ordered.""" quantity: Int! """ Quantity already returned (for refunds / partial returns). 0 on a fresh order. """ quantityReturned: Int! """Unit price actually charged, in the order's currency.""" price: Float! """ Catalog price at the time of order (before any per-customer or coupon adjustments). Useful for computing realised discount per line. """ originalPrice: Float! """Tax rate applied to this line as a percentage (e.g. 24.0 = 24% VAT).""" tax: Float! """ Comma- or JSON-encoded snapshot of the variant stock-id selection (e.g. for size/colour SKUs). """ stockVariants: String """ Line type. Example values seen in production: - 'item' (default): regular product line - 'refund': return-credit line subtracted from the order total Tenants may extend with custom types ('shipping', 'fee', 'comment'). """ lineType: String """ Free-form comment captured at checkout (gift message, configuration notes, etc.). """ comment: String """ Module-specific custom-options payload (JSON-encoded). Set when the product has configurable options the shopper filled out at checkout. """ customOptions: JSON """ Snapshot of the product's category memberships at order time (comma-separated category ids). """ productCategories: String """ Per-line product weight in kilograms (used for shipping calculation when line-level weights matter). """ productWeight: Float """ Per-line warehouse location code captured at order time (e.g. shelf identifier). """ productLocation: String """ Filename of a per-line custom image uploaded by the shopper (for personalised / printed products). Null when the line carries no custom image. """ variableImage: String """ True (1) if the customer accepted product substitution for this line: warehouse may ship a similar product when the requested variant is unavailable. 0 = strict matching. """ allowSubstitute: Int! """ True (1) if a confirmation step was applied to this line during pick-and-pack (warehouse confirmed the SKU + quantity). 0 = unconfirmed. """ confirmFlag: Int! """ Confirmed quantity captured during pick-and-pack. May differ from the ordered quantity when shortages or substitutions occurred. """ confirmProductsQuantity: Int! """ True (1) if the line was added by an automatic process (auto-bundle expansion, accessory rule, etc.) rather than by the shopper. 0 = manually added. """ autoComment: Int! """ Configurable variants selected on this line. Empty array for products without options or when the order line is a fee / refund / comment row. """ variants: [OrderItemVariant!]! } """ A configurable-product option selected on an order line, returned by the addOrdersProductsVariant / updateOrdersProductsVariant mutations. Stored on a separate table (orders_products_variants) from the line itself so a single line can carry many variant selections. Read-side: this same data is exposed through OrderItem.variants on the read query path. Use the mutations here when you need to add, modify, or remove option selections on an already-placed order from the admin panel or an integration. """ type OrdersProductsVariant { """Database row id of the variant selection.""" id: ID! """Order the variant belongs to (denormalised for filtering).""" orderId: Int! """ Order line (OrderItem / OrdersProduct) this variant selection belongs to. """ orderProductId: Int! """Product option id (matches a row in the catalog options table).""" optionId: Int! """ Display label of the option as shown to the shopper at checkout (e.g. 'Size', 'Engraving'). """ optionName: String! """ Selected value as shown to the shopper (e.g. 'XL', 'Happy Birthday Anna'). """ optionValue: String! """ Price delta this variant contributes to the line, interpreted via pricePrefix. """ price: Float! """ How the price delta is applied. Example values: - '+' (most common): additive — added to the base product price - '' (empty): replacement — overrides the base price entirely """ pricePrefix: String } """ Input shape for attaching a new variant selection to an existing order line. """ input AddOrdersProductsVariantInput { """Order this line belongs to.""" orderId: ID! """Order line to attach the variant to.""" orderProductId: ID! """Product option id from the catalog.""" optionId: Int! """Display label of the option.""" optionName: String! """Selected value.""" optionValue: String! """Price delta this variant contributes.""" price: Float! """How price is applied — defaults to '+' (additive) when omitted.""" pricePrefix: String } """ Input shape for partially updating an existing variant selection. All fields are optional — only provided fields are written. Used for admin post-order corrections (e.g. fixing a typo in the captured option name, or adjusting a price delta after the fact). """ input UpdateOrdersProductsVariantInput { """ Move the variant to a different order (rare; used in split / merge flows). """ orderId: ID """Move the variant to a different order line.""" orderProductId: ID """Update the catalog option id reference.""" optionId: Int """Update the option display label captured at order time.""" optionName: String """Update the selected value captured at order time.""" optionValue: String """Update the price delta this variant contributes.""" price: Float """ Update how the price delta is applied ('+' additive or '' replacement). """ pricePrefix: String } """ Write-shaped representation of an order line, returned by the addOrdersProduct / updateOrdersProduct mutations. Maps to the same underlying orders_products row that OrderItem reads from, but excludes the nested variants list (manage variants separately via the addOrdersProductsVariant / updateOrdersProductsVariant / removeOrdersProductsVariant mutations). Use OrderItem for read flows; use OrdersProduct + the matching mutations when modifying lines on an already-placed order from the admin panel or an integration. """ type OrdersProduct { """Database row id of the line (orders_products_id).""" id: ID! """Order this line belongs to.""" orderId: Int! """Catalog product id this line references. Null for free-form lines.""" productId: Int """ Product SKU / model identifier captured at order time. Snapshotted from products.products_model so the order stays stable when the catalog is later edited. """ productModel: String """EAN / barcode captured at order time.""" productEan: String """ Display name shown on the receipt and admin order detail. Snapshotted at order time. """ productName: String! """Unit price actually charged, in the order's currency.""" price: Float! """ Catalog price at the time of order, before per-customer or coupon adjustments. """ originalPrice: Float! """Tax rate applied to this line as a percentage (e.g. 24.0 = 24% VAT).""" tax: Float! """Quantity ordered on this line.""" quantity: Int! """Quantity already returned. 0 on a fresh line.""" quantityReturned: Int! """Comma- or JSON-encoded snapshot of the variant stock-id selection.""" stockVariants: String """Line type. Example values: 'item' (default), 'refund'.""" lineType: String """ Free-form comment captured at checkout (gift message, configuration notes). """ comment: String! """ Module-specific custom-options payload (JSON-encoded) for configurable products. """ customOptions: JSON } """ Input shape for adding a new line to an existing order via addOrdersProduct. """ input AddOrdersProductInput { """Order to attach the new line to.""" orderId: ID! """ Catalog product id, when the line corresponds to a real product. Null for free-form lines. """ productId: Int """Product SKU / model identifier to capture on the line.""" productModel: String """EAN / barcode to capture on the line.""" productEan: String """Display name shown on the receipt. Required.""" productName: String! """Unit price to charge.""" price: Float! """Catalog price for discount-tracking. Falls back to price when omitted.""" originalPrice: Float """Tax rate as percentage. Falls back to 0 when omitted.""" tax: Float """Quantity to add on the new line.""" quantity: Int! """Comma- or JSON-encoded snapshot of the variant stock-id selection.""" stockVariants: String """ Line type — defaults to 'item'. Pass 'refund' for a return-credit line. """ lineType: String """ Free-form comment to attach to the line (gift message, configuration notes). """ comment: String """ Module-specific custom-options payload (JSON-encoded) when the product carries configurable options. """ customOptions: JSON } """ Input shape for partially updating an existing order line via updateOrdersProduct. All fields are optional — only the provided fields are written. Used for admin post-order corrections (price adjustment, quantity fix, attach a custom comment). """ input UpdateOrdersProductInput { """ Move the line to a different order id (rare; used for split / merge operations). """ orderId: ID """Update the catalog product id reference.""" productId: Int """Update the product model snapshot.""" productModel: String """Update the EAN snapshot.""" productEan: String """Update the display name shown on the receipt.""" productName: String """Update the unit price charged.""" price: Float """Update the catalog reference price.""" originalPrice: Float """Update the tax rate.""" tax: Float """Update the quantity ordered.""" quantity: Int """Update the variant stock-id snapshot.""" stockVariants: String """Update the line type.""" lineType: String """Update the line comment.""" comment: String """Update the custom-options payload.""" customOptions: JSON } """ A single line on the order's totals breakdown — the rendered "totals block" at the bottom of an invoice or order detail (Subtotal, Tax, Shipping, Coupon discount, Grand total). Each row represents one contribution to the final order amount, sorted by sortOrder, and carries the formatted display text the storefront / receipt renders verbatim. Mutations don't write to OrderTotal directly: totals are recomputed by the order pipeline whenever the underlying lines or coupons change. Use this type for read-only reporting and rendering. """ type OrderTotal { """Database row id of the totals entry (orders_total_id).""" id: ID! """Order this totals row belongs to.""" orderId: Int! """ Display label shown next to the value (e.g. 'Subtotal', 'Tax 24%', 'Shipping'). """ title: String! """ Pre-formatted display value rendered next to the title (e.g. '€ 49.90', 'Free'). Includes currency symbol and locale formatting. """ text: String! """ Numeric amount in the order's currency. Sum of all rows of class 'Total' equals the order's final amount. """ value: Float! """ Numeric amount excluding any returned/refunded portion. Equals value when the order has not been partially returned. """ valueUnreturned: Float! """ Tax rate (percentage) applied to compute taxAmount, when this row represents a taxed total. Null for tax-free totals (e.g. shipping discounts). """ taxRate: Float """ Tax portion of value, in the order's currency. Null when the row is itself a tax line. """ taxAmount: Float """ Total category. Example values seen in production: - 'Total': grand total of the order - 'Subtotal': line-items subtotal before fees and taxes - 'Tax': aggregated tax line (or one row per VAT rate) - 'Shipping': shipping cost line - 'DiscountCoupon': coupon-applied discount - 'Cod': cash-on-delivery surcharge - 'ot_klarna_fee', 'ot_collector_invoice_fee', 'ot_kreditor_fin_payment_chg': payment-provider fees - 'ot_3for2': 3-for-2 promotional discount - 'ot_custom', 'ot_custom_2': tenant-defined custom totals Tenants may extend with additional 'ot_*' modules. """ class: String! """ Display sort order. Lower values render first; the totals block typically goes Subtotal → Tax → Shipping → Discount → Total. """ sortOrder: Int! } """ Localized text and SEO metadata for one Service page, in one language. Each Service has one ServiceDescription per (service, languageId) pair — the storefront picks the row matching the active language at render time. Carries the visible name, slug-style URL, the rich-text body rendered on the page, and the meta-tags emitted into the page head. """ type ServiceDescription { """Language id this row is for (resolves against the languages query).""" languageId: Int! """Localized service / page name shown in navigation and the page header.""" name: String """Slug-style URL fragment for the page (e.g. 'shipping-and-returns').""" url: String """ Long-form rich-text body rendered on the page. Typically HTML; tenants who run a separate CMS may keep this empty and render from the CMS instead. """ html: String """Localized <title> tag for the page.""" metaTitle: String """Localized <meta name='description'> for the page.""" metaDescription: String } """ A static-content page in the storefront's CMS layer — typically About Us, Shipping & Returns, FAQ, Terms, Contact, or any tenant-defined informational page. Each page lives under one ServiceCategory (categoryId) and carries per-page visual / behaviour fields plus a ServiceDescription per language for the localized copy. Per-page custom CSS (css) and external script / stylesheet hooks (externalJs / externalCss) let tenants customize individual page layouts without touching the storefront theme — useful for marketing landing pages with bespoke visual treatments. The googleFeed flag controls whether the page surfaces in the Google Merchant feed; the displayFooter flag controls whether it appears in the storefront's footer link block. Distinct from the catalog product / category surfaces — services hold static content (information pages, policies), not buyable goods. For category-tree navigation see serviceCategories. """ type Service { """Database row id of the service / page.""" id: ID! """Id of the parent ServiceCategory.""" categoryId: Int """ Filename of the page hero / illustration image, served from the tenant's service-image directory. """ image: String """ Display sort order within the parent category. Lower values render first. """ sortOrder: Int """ Whether the page appears in the storefront footer link block. 1 = visible in footer, 0 = not in footer. """ displayFooter: Int """ Whether the page is included in the Google Merchant feed. 1 = include, 0 = exclude. """ googleFeed: Int """ Per-page custom CSS injected into the rendered page. Used to tweak layout for one page without changing the global theme. """ css: String """ External JavaScript URL to load on this page (e.g. a third-party widget script). """ externalJs: String """External stylesheet URL to load on this page.""" externalCss: String """ Inline SVG markup for the page icon. Stored as SVG source — the storefront drops it directly into the page. """ svgIcon: String """Localized name / URL / body / meta rows, one per language.""" descriptions: [ServiceDescription] """ Resolved parent ServiceCategory, hydrated alongside the service in the same response. """ category: ServiceCategory } """ Localized text and SEO metadata for one ServiceCategory, in one language. Each category has one ServiceCategoryDescription per (category, languageId) pair — the storefront picks the row matching the active language at render time. """ type ServiceCategoryDescription { """Language id this row is for (resolves against the languages query).""" languageId: Int! """ Localized category name shown in navigation, breadcrumbs, and the page header. """ name: String """Slug-style URL fragment for the category landing page.""" url: String """Long-form rich-text body rendered on the category landing page.""" html: String } """ A grouping of static-content service pages — the top level of the storefront's CMS layer. Holds About-us / Help / Terms / Contact-style pages organized into a navigation tree (parentId nests categories under each other), with display flags for inclusion in the storefront footer and per-language description rows for the localized copy. Distinct from category (the product-catalog category tree) — service categories are CMS pages, while categories group products. For the individual service / page records living under each category, see the services query. """ type ServiceCategory { """Database row id of the service category.""" id: ID! """Id of the parent category, or 0 for top-level categories.""" parentId: Int """ Display sort order within the parent category. Lower values render first. """ sortOrder: Int """ Whether the category appears in the storefront footer navigation. 1 = visible in the footer, 0 = navigation-only (not in footer). """ displayFooter: Int """ Inline SVG markup for the category icon. Stored as the SVG source rather than a filename — the storefront drops it directly into the page. """ svgIcon: String """Localized name / URL / body rows, one per language.""" descriptions: [ServiceCategoryDescription] """Child categories nested under this one. Empty for leaf categories.""" children: [ServiceCategory] } """ Localized text for one Review, in one language. A review can have multiple ReviewDetail rows (one per language) when the storefront supports translated review bodies; carries the localized title, the body text, and the optional vendor reply. """ type ReviewDetail { """ Language id this row is for (resolves against the languages query). Null on tenants that don't localize reviews. """ languageId: Int """Review headline shown above the body.""" title: String """ Long-form review body (rich or plain text depending on the tenant's input field). """ detail: String """ Vendor reply published below the customer's body. Null when the vendor hasn't responded. """ answer: String } """ Date metadata for a Review. Captured at submission, optionally at vendor-reply time, and a snapshot of the order date when the review is tied to a verified purchase. Returned as a nested object so the three timestamps can be hydrated together without flattening into the parent Review type. """ type ReviewDates { """Timestamp the review was submitted.""" createdAt: String """ Order date the review references when the reviewer is a verified buyer. Null for non-verified reviews. """ orderDate: String """Timestamp the vendor reply was posted. Null when there's no reply yet.""" answerDate: String } """ A customer review of a product, with optional vendor reply via ReviewDetail.answer. Reviews progress through a moderation lifecycle (statusId / statusCode) before they're published — typical states are pending → approved (visible) or rejected (hidden). Verified-buyer flagging is automatic: the platform sets verifiedBuyer = true when the reviewer's customer id is found on a previous order for the same product. Used to render product detail page review widgets, the per-product review carousels in product cards, and the admin moderation queue. Aggregate scores (count of approved reviews + average rating) are exposed via reviewSummaries. """ type Review { """Database row id of the review.""" id: ID! """Product the review is about.""" productId: ID! """Reviewer's customer id. Null for guest / unauthenticated reviews.""" customerId: Int """ Public display name shown alongside the review (the reviewer's chosen handle). """ nickname: String """ Reviewer's country at submission time (typically a 2-letter ISO code). Used for region-aware moderation and aggregate filtering. """ country: String """ Whether the reviewer would recommend the product to others. Optional star-recommendation flag captured alongside the rating. """ isRecommended: Boolean """ Whether the reviewer is a verified purchaser. Auto-set when the reviewer's customer id is found on a previous order for the product. """ verifiedBuyer: Boolean """ Tenant-defined counter on the review row. Semantics vary across tenants — most commonly 'helpful votes' in production, but treated as opaque by core code. """ count: Int """ Numeric moderation status. Resolve to a stable string code via statusCode; the platform's canonical lifecycle is pending → approved → rejected. """ statusId: Int """ Stable internal moderation status code (e.g. 'pending', 'approved', 'rejected'). Use this for routing rules rather than statusId, which may collide across tenants. """ statusCode: String """Submission / vendor-reply / order-date timestamps.""" dates: ReviewDates """Localized title / body / answer rows, one per language.""" details: [ReviewDetail] } """ Aggregate review metrics for one product. Recomputed by the review pipeline whenever a review's moderation state flips into / out of 'approved' so the storefront can render review counts and average star ratings without scanning all reviews on every page render. """ type ReviewSummary { """Product the metrics aggregate over.""" productId: ID! """Total count of approved reviews for this product.""" reviewsCount: Int """ Aggregate star rating on a 1–5 scale. Stored as int; the storefront formats fractional stars from this aggregate as needed. """ ratingSummary: Int } """ Lifetime impression / click totals for one banner. Aggregated across all history rows for the banner. For a per-day breakdown (e.g. to render a performance chart) see Banner.historyByDate, which buckets the same underlying banners_history rows by day. """ type BannerHistory { """Total impressions across the banner's full history.""" impressions: Int! """Total clicks across the banner's full history.""" clicks: Int! } """ A storefront banner — a clickable promotional image rendered in a banner zone (homepage hero, sidebar, category banner, etc.). Each banner belongs to one group, links to a landing URL, and tracks impressions and clicks for performance reporting. Use the banners query to enumerate every banner the tenant has defined, bannersByGroup to fetch only the banners in one zone, and the Banner.historyByDate field for daily click / impression series. """ type Banner { """Database row id of the banner (banners_id).""" id: ID! """ Display title used as the banner's accessible label and admin-side identifier. """ title: String! """Destination URL the banner links to when clicked.""" url: String! """ Free-form group code identifying which storefront zone the banner belongs to (e.g. 'home', 'sidebar', 'category-1'). Tenants define their own groups. """ group: String! """ Visibility flag stored as int. 1 = active (rendered on the storefront); 0 = inactive (hidden but kept for history). """ status: Int! """ Filename of the banner image, served from the tenant's banner-image directory. Null when the banner is text-only. """ image: String """Lifetime impression and click totals for this banner.""" history: BannerHistory! """ Per-day impression and click history for this banner, newest day first. Pass startDate / endDate to bracket the chart window — the underlying query truncates banners_history_date to a date and groups rows by day, so partial days at the edge of the window count correctly. """ historyByDate( """Inclusive lower bound on the banner-history date. ISO-8601 date.""" startDate: String """Inclusive upper bound on the banner-history date. ISO-8601 date.""" endDate: String """Maximum number of day-buckets to return.""" limit: Int """Offset for pagination across day-buckets.""" offset: Int ): [BannerHistoryEntry!]! } """ Localized text and SEO metadata for one FAQ category, in one language. Each FaqCategory has one FaqCategoryDescription per (category, languageId) pair — the storefront picks the row matching the active language at render time. Carries the visible name, the slug-style URL fragment, the long category description, and the SEO meta-tags emitted into the FAQ landing page head. """ type FaqCategoryDescription { """Language id this row is for (resolves against the languages query).""" languageId: Int! """Localized category name shown in the FAQ navigation and page header.""" name: String """Slug-style URL fragment for the category landing page.""" url: String """Long-form category description shown above the question list.""" description: String """Localized <title> tag for the category landing page.""" metaTitle: String """Localized <meta name='description'> for the category landing page.""" metaDescription: String } """ Localized text and SEO metadata for one FAQ question, in one language. Each FaqQuestion has one FaqQuestionDescription per (question, languageId) pair. Carries the question itself, the long answer, an optional shortAnswer used in collapsed / preview surfaces, the per-question slug, and SEO meta-tags emitted when the question gets its own landing page (gated by FaqQuestion.enableUrl). """ type FaqQuestionDescription { """Language id this row is for.""" languageId: Int! """The question text shown to shoppers.""" question: String """Long-form answer body. Typically rich text.""" answer: String """ Short answer used in collapsed / accordion surfaces and in card previews. """ shortAnswer: String """ Slug-style URL fragment for the per-question landing page (used when enableUrl = 1). Looked up by the faqQuestion(slug:) query. """ url: String """Localized <title> tag for the per-question landing page.""" metaTitle: String """Localized <meta name='description'> for the per-question landing page.""" metaDescription: String } """ One FAQ entry — a question with one or more localized answers. Lives under one or more FaqCategory parents (the categories array makes the question-to-category relationship many-to-many via the faq_to_category bridge), and can optionally surface as its own SEO landing page (when enableUrl = 1, the question's slug from FaqQuestionDescription.url becomes routable). Used to render the storefront's help center, accordion-style FAQ blocks on product / category pages, and AI-assistant grounding (the same content frequently feeds the chat widget's vector store, see Domain.openaiVectorStoreId). """ type FaqQuestion { """Database row id of the question.""" id: ID! """ Optional illustration filename, served from the tenant's FAQ-image directory. """ image: String """ Display sort order within each parent category. Lower values render first. """ sortOrder: Int """ Visibility flag. 1 = visible on the storefront; 0 = hidden (kept for editing without going live). """ visibility: Int """ Search-engine indexability flag for the per-question landing page. 1 = indexable, 0 = noindex. """ googleIndex: Int """ Whether the question gets its own SEO landing page at the slug captured in FaqQuestionDescription.url. 1 = enabled, 0 = the question only renders inside category pages. """ enableUrl: Int """ Parent category ids — many-to-many. One question can live under multiple FAQ categories. """ categories: [Int] """Localized question / answer rows, one per language.""" descriptions: [FaqQuestionDescription] } """ A grouping of FAQ questions — the top level of the help-center tree. Each FaqCategory carries display assets (icon, image), visibility / SEO flags, sort order, and a localized FaqCategoryDescription per language. The questions field is hydrated when the parent faqCategories query is called with the appropriate filters (visibleOnly / includeHiddenQuestions / sortByOrder shape what it contains). """ type FaqCategory { """Database row id of the category.""" id: ID! """Filename of the category icon (small inline glyph).""" icon: String """Filename of the category hero image.""" image: String """Visibility flag. 1 = visible on the storefront; 0 = hidden.""" visibility: Int """ Search-engine indexability flag for the category landing page. 1 = indexable, 0 = noindex. """ googleIndex: Int """Display sort order. Lower values render first.""" sortOrder: Int """Localized name / URL / description / SEO rows, one per language.""" descriptions: [FaqCategoryDescription] """ Questions belonging to this category, hydrated from the faq_to_category bridge. Ordered by question sortOrder when sortByOrder is true on the query, otherwise by id. """ questions: [FaqQuestion] } """ One dynamic-discount tier inside a customer group. Dynamic discounts apply at the basket / catalogue level on top of the group's base discount, gated by manufacturer / category / date window — letting tenants run group-scoped promotions like 'Members get an extra 10 € off Brand X this month' without rewriting the group's main discount. Distinct from CustomerGroupPriceRule — that surface fixes a specific product's price for the group, while dynamic discounts apply across whole product slices defined by manufacturer / category. """ type CustomerGroupDynamicDiscount { """Database row id of the dynamic-discount entry.""" id: ID! """Id of the parent customer group this discount applies to.""" groupId: String """Discount amount, in the tenant's default currency (not a percentage).""" amount: Float """ Inclusive lower bound on when the discount is active. Null = no lower bound. """ dateStart: String """ Inclusive upper bound on when the discount is active. Null = no upper bound. """ dateEnd: String """ Manufacturer code / id this discount is scoped to. Null = applies across all manufacturers. """ manufacturer: String """ Category code / id this discount is scoped to. Null = applies across all categories. """ category: String """ Display sort order when the admin UI lists multiple dynamic-discount rows on a group. """ sort: Int } """ One product-level price override inside a customer group. Replaces the product's regular price with rule_price for members of the group when the rule is active (validFrom / validTo bracket). Used to run wholesale / contract pricing where individual products carry hand-set prices for a group, not a percentage off the regular price. For variant-level overrides, productStockId points at one variant in products_stock; null means the override applies to the product as a whole. For percentage / formula-based group discounts see CustomerGroup.discountAmount + discountType; for cross-group lookup see customerGroupPriceList. """ type CustomerGroupPriceRule { """Database row id of the price rule.""" id: ID! """Id of the parent customer group this rule applies to.""" groupId: String """Product the rule overrides the price of.""" productId: Int """ Variant (products_stock_id) the rule scopes to. Null = applies to the product as a whole, regardless of variant. """ productStockId: Int """ Replacement price for the product / variant, in the tenant's default currency. Replaces the product's regular price for members of the group. """ price: Float """ Inclusive lower bound on when the rule is active. Null = no lower bound. """ validFrom: String """ Inclusive upper bound on when the rule is active. Null = no upper bound. """ validTo: String """ Resolved parent customer group, fetched on demand when the field is selected. Lets one round-trip pull a price-rule plus the group it belongs to (description, discount type, validity window). """ customerGroup: CustomerGroup """ Resolved product the rule applies to, fetched on demand when the field is selected. Returns the product's name / model / image / base price for surface display alongside the rule. """ product: Product } """ A pricing / discount scheme applied to a slice of customers — a B2B tier, a wholesale partner channel, a loyalty group, etc. Customers belong to zero or more groups (via Customer.group, JSON-array of group ids), and the storefront / pricing engine layers the group's discount, per-product price-list rules, and dynamic-discount tiers on top of each shopper's regular price. Carries the headline-level configuration (discount amount + type, B2B / consumer gating via customerType, validity window) plus the two sub-collections that hang off it: priceList (per-product overrides) and dynamicDiscounts (manufacturer / category-scoped extras). """ type CustomerGroup { """Database row id of the customer group (groups_id).""" id: ID! """ Display description of the group, shown to admins (e.g. 'Wholesale tier 1', 'Loyalty members'). """ description: String """ Discount amount, interpreted via discountType (currency or percent depending on type). """ discountAmount: Float """ How discountAmount is applied. Example values: - 'fixed' — currency amount off the product price - 'percent' — percentage off the product price - 'shipping' — applies as a shipping subsidy / free-shipping flag """ discountType: String """ Inclusive lower bound on when the group's discount is active. Null = no lower bound. """ dateStart: String """ Inclusive upper bound on when the group's discount is active. Null = no upper bound. """ dateEnd: String """ Minimum order value the basket must reach before the group's discount applies. 0 = no threshold. """ minOrder: Float """ How minOrder is interpreted. Example values: - 'price' — basket value in the tenant's default currency - 'quantity' — total item count in the basket """ minOrderType: String """ Customer-type gate. The customerGroups resolver also reads the request's X-Customer-Type header and only returns groups whose customerType matches 'all' or the header value. Example values: - 'all' — applies to every customer - 'vatid' — B2B / VAT-id-bearing customers only - 'regular' — consumer accounts (no VAT id) only """ customerType: String """ Count of products that have a CustomerGroupPriceRule against this group. A precomputed convenience for the admin's group editor — no need to count rule rows separately. """ discountedProducts: Int """ Group-shape classifier — how the group's discount is structured. Example values: - 'fixed' — single discountAmount applied across the group - 'dynamic' — discount comes from CustomerGroupDynamicDiscount tiers - 'matrix' — manufacturer × category matrix of discounts - 'list' — per-product CustomerGroupPriceRule overrides """ groupsType: String """ Dynamic-discount tiers attached to the group. Empty when the group doesn't run dynamic discounts. """ dynamicDiscounts: [CustomerGroupDynamicDiscount] """ Per-product price overrides for the group. Empty when the group doesn't run a price list. """ priceList: [CustomerGroupPriceRule] } """ An audit-log entry recording one field-level change to a customer record (or to one of its address-book / customer-info side-tables). The customers_history table is generic enough to capture edits across multiple source tables — sourceTable identifies which table the row originated from, fieldName / oldValue / newValue capture the diff, and the userId / userName columns attribute the edit to the admin who made it. Used by the admin's customer detail view's history tab, by GDPR / data-subject-access reports, and by any after-the-fact investigation ('who changed this customer's email last week?'). For the broader cross-module audit log see actionRecorderEntries; for product-level edits see adminTrackingCreate; this surface is the customer-record-specific change log. """ type CustomerHistoryEntry { """Database row id of the history entry.""" id: ID! """Customer the change was made on.""" customerId: Int! """ Address-book row id when the change was on an address row. Null when the change was on the customer record itself or on a non-address side-table. """ addressBookId: Int """Id of the admin who made the change.""" userId: Int! """ Username of the admin, captured at write time so the row stays readable if the admin is later deleted. """ userName: String! """ Source table the changed row belongs to. Example values seen in code: - 'customers' — top-level customer record - 'address_book' — one of the customer's saved addresses - 'customers_info' — activity-stats sidecar (login counters, etc.) Tenants may extend; treat as free-form code rather than a fixed enum. """ sourceTable: String! """ Column name within sourceTable that changed (e.g. 'customers_email_address', 'entry_postcode'). """ fieldName: String! """Value before the change. Null when the field was previously empty.""" oldValue: String """Value after the change. Null when the field was cleared.""" newValue: String """ Admin-page path that made the change (e.g. '/admin/customers/edit'). Useful for tracing which UI flow caused the edit. """ page: String """Timestamp the change was recorded.""" dateAdded: String! } """ An ERP-mirrored snapshot of one customer record. The customers_api table caches the customer master data the tenant's ERP owns — Netvisor / Lemonsoft / Tehden / SAP / Tisma depending on the tenant — so the storefront and admin can reference contract-side identifiers, payment terms, credit limits, sales codes, and discount-group assignments without round-tripping to the ERP for every read. The Finnish-language column names (asiakaskoodi = customer code, alennusryhma = discount group, ytunnus = business id) reflect the Finnish-ERP origin of the schema; the table is read-only from the GraphQL surface — the writer is an async ERP-side sync job that pushes updates into this cache as the source-of-truth ERP changes. For the platform-side customer record (logins, addresses, orders, group membership), see Customer / customers. customersApi is the ERP-shaped counterpart, joined on the asiakaskoodi key. """ type CustomersApiCustomer { """ ERP-side primary customer code (Finnish 'asiakaskoodi'). The stable join key into the platform-side Customer.apiId. """ asiakaskoodi: String! """Primary recipient name as stored in the ERP.""" name: String """ Secondary name line — typically a contact person, department, or 'attention to' line in the ERP. """ secondaryName: String """First street-address line (ERP shipping/main address).""" addressLine1: String """Second street-address line.""" addressLine2: String """ Third street-address line — typically postcode + city in the ERP convention. """ addressLine3: String """ First delivery-address line — separate from the main address when ERP tracks ship-to and bill-to as different parties. """ deliveryName1: String """Second delivery-address line.""" deliveryName2: String """Third delivery-address line.""" deliveryName3: String """ ERP id of the billing customer when this record is a delivery sub-account that bills to a parent customer. """ billingCustomerId: Int """ ERP-defined payment-term code (e.g. 'NET14', 'NET30'). Drives invoice due-date calculation. """ paymentTerm: String """ ERP customer-group code (e.g. wholesale tier, regional segment). Used by the storefront pricing engine when mirrored into platform-side customer groups. """ customerGroup: String """ ERP chain / parent-group code when the customer belongs to a chain account. """ chain: String """ ERP-side sales-code / sales-rep id (the salesperson responsible for the account). """ salesCode: String """ Credit limit set in the ERP, in the customer's currency. Stored as a string because the ERP may format it (e.g. '10000.00'). """ creditLimit: String """ ERP delivery-block code. When set, the ERP has flagged this customer as not-to-be-delivered (e.g. for outstanding invoices). """ deliveryBlock: String """ERP-side primary email.""" email: String """ERP-side website / URL.""" website: String """ERP-side phone number.""" phone: String """ ERP-side fax number (legacy field; rarely populated on modern accounts). """ fax: String """ ERP delivery-method code (e.g. 'pickup', 'parcel', 'truck'). Maps to a shipping module on the storefront. """ deliveryMethod: String """ ERP discount-group code ('alennusryhma'). The pricing engine looks up the discount percentage by this code. """ discountGroup: String """ Discount percentage from the ERP for this customer (the customer-level baseline, before group / contract overrides). """ discountPercent: Float """ Secondary discount percentage. ERPs commonly use this for stacking a category-level or campaign-level discount on top of the primary discount. """ secondaryDiscountPercent: Float """ ERP-side language code (e.g. 'fi', 'sv', 'en'). Used to localize ERP-generated documents (invoices, packing slips). """ languageCode: String """First free-form ERP comment field. Tenant-defined notes.""" comment1: String """Second free-form ERP comment field.""" comment2: String """Primary contact-person name on the customer record.""" contactPerson: String """Primary contact-person id (ERP-side reference into a contacts table).""" contactPersonId: Int """ Timestamp of the customer's most recent storefront login, mirrored back into the ERP cache. """ lastLoginAt: String """ VAT-liability code from the ERP (e.g. 'standard', 'reverse-charge', 'exempt'). Drives tax behaviour on invoices. """ vatLiability: String """ERP-side default currency code for the customer (ISO 4217).""" currency: String """Timestamp of the most recent ERP-side edit to the record.""" modifiedAt: String """ Business id (Finnish 'ytunnus' / VAT number). Used for B2B invoicing and EU VAT validation. """ businessId: String """Default invoice address line stored on the ERP record.""" billingAddress: String """ E-invoice intermediary / operator code ('valityspalvelu' in Finnish e-invoicing). Names the operator routing the customer's e-invoices. """ intermediaryService: String """ Netvisor-side customer key (Finnish ERP integration). Used to look up the customer in Netvisor's API; 0 / null for customers not mirrored from Netvisor. """ netvisorKey: Int } """ One page-view captured by the storefront's browse-history tracker. Every product, category, manufacturer, and search-result page rendered by the storefront writes a row here with the visiting session's key (so anonymous and logged-in journeys can both be reconstructed), the entity that was viewed, page-load timing, geo-IP country, and any variant filters that were active. Used to power 'Recently viewed' on the storefront, internal-search analytics, conversion-funnel reporting, and the homepage's top-picks curation. Distinct from actionRecorderEntries — this surface tracks visitor browsing for product / merchandising analytics, while the action recorder tracks admin and auth events for security audit. """ type BrowseHistory { """Database row id of the page-view entry.""" id: ID! """ Customer id when the visitor was logged in. Null for anonymous visitors — the sessionKey still threads their journey across pages. """ customerId: Int """ Storefront session key that ties together page views from one browser session, even before login. """ sessionKey: String! """ Server-side page-load time in milliseconds, captured at write time. Used for performance regression tracking. """ loadTimeMsec: Int! """ Category id when the page-view was a category listing. Null when the visitor wasn't on a category page. """ categoryId: Int """ Product id when the page-view was a product detail page. Null when the visitor wasn't on a product page. """ productId: Int """ Manufacturer id when the page-view was filtered by a manufacturer. Null when no manufacturer filter was active. """ manufacturerId: Int """ Color filter active on the page (option-value id from products_options_values). 0 / null when no color filter was applied. """ color: Int """ Size filter active on the page (option-value id from products_options_values). 0 / null when no size filter was applied. """ size: Int """ Two-letter geo-IP country code resolved at request time (e.g. 'FI', 'US'). Useful for regional behaviour analytics. """ geoipCountry: String! """ Visitor IP address at the time of the page view. Captured for fraud / abuse forensics. """ ipAddress: String! """Server timestamp of the page view.""" date: String! """ Unix timestamp of the page view (seconds since epoch). Mirrors the date field in epoch form for time-series analysis. """ timestamp: Int """ Whether this page view was rendered from the storefront's curated 'top picks' surface (homepage hero / featured area) rather than a regular browse path. Used to attribute conversions back to merchandising decisions. """ topPicks: Boolean! """ Search-keywords slot on the row. In practice usually empty / null on browse_history rows — internal-search keyword logging lives in the dedicated searched_keywords table. Kept on this surface for legacy-compat callers. """ keywords: String } """ A standalone billing-address entry belonging to a customer. Distinct from AddressBook — that surface holds shipping addresses with optional alternate billing fields per row, while BillingAddress is a dedicated invoice-recipient book customers can populate independently of where they ship. Used primarily for B2B accounts that need multiple invoice recipients (e.g. an organization with several cost centres / departments billed separately) without dragging a shipping address along. The fields mirror the billing_* half of AddressBook plus the e-invoice routing fields used in Finnish / Nordic invoicing. """ type BillingAddress { """Database row id of the billing-address entry (billing_address_id).""" id: ID! """Customer this billing address belongs to.""" customerId: Int! """Company name on the invoice.""" company: String """Recipient first name on the invoice.""" firstname: String """Recipient last name on the invoice.""" lastname: String """Street address on the invoice.""" streetAddress: String """Postal code on the invoice.""" postcode: String """City on the invoice.""" city: String """State / region on the invoice.""" state: String """Country id on the invoice.""" countryId: String """Zone id on the invoice.""" zoneId: String """ VAT / business id printed on the invoice. Required for B2B invoices in EU jurisdictions. """ vatid: String """ E-invoice operator id (Finnish Verkkolasku operator). Used for routing electronic invoices through an operator. """ operatorId: Int """ E-invoice address (Finnish OVT-tunnus / EDI). Paired with operatorId for e-invoice delivery. """ invoiceAddress: String """Free-form reference / PO number to print on the invoice.""" reference: String """ Whether this billing address requires a non-empty reference at checkout. The storefront enforces the flag before letting the order through. """ referenceRequired: Boolean! } scalar JSON """ A job queued for an outbound third-party API call — typically an ERP / PIM / marketplace synchronisation push (Netvisor / Lemonsoft / Akeneo / shipping-carrier APIs). The platform writes a row here whenever it needs to dispatch a request to an external system, and a worker drains the queue and replays each job against its target. Successful jobs leave error null; failed jobs capture the error for retry / inspection. Use this surface to inspect the queue from admin tooling and oncall runbooks ('what's stuck?'), to reconcile against external system status, and to filter to specific failure modes via the status argument. """ type ApiProcessJob { """Database row id of the queued job.""" id: ID! """ Worker-side job id when the worker assigns one. Null until the worker picks the row up. """ jobId: String """ Identifier of the target third-party API (e.g. 'netvisor', 'lemonsoft', 'akeneo', 'tisma'). """ api: String! """ API endpoint or path the job will invoke (e.g. '/customer.nv', '/products'). """ endpoint: String! """ HTTP method to use (typically 'POST' or 'PUT'). Null for non-HTTP integrations. """ method: String """ Request payload to send, as JSON. The worker serializes this to the wire format the target API expects. """ data: JSON! """Timestamp the job was queued.""" createdAt: String! """ Error text from the most recent attempt; null while the job has not failed. """ error: String """ Computed status derived from the error column: - 'queued': job has not failed (error is null) — pending or successfully processed - 'failed': last attempt produced an error """ status: String! } """ An address-book entry belonging to a customer. Holds the shipping address in the entry_* columns plus an optional alternate billing address in the billing_* columns of the same row — a customer who ships to one address but invoices to another (common for B2B accounts) keeps both halves together here. When the billing_* fields are null/empty, billing falls back to the shipping side. For a customer's standalone list of billing-only addresses (multiple invoice recipients without a paired shipping address), see billingAddresses, which reads from a separate billing_address_book table. """ type AddressBook { """Database row id of the address-book entry (address_book_id).""" id: ID! """Customer this address belongs to.""" customerId: Int! """ Salutation / gender code for the recipient (e.g. 'm', 'f'). Tenant-defined; many tenants leave this null. """ gender: String """ Company name on the shipping side. Set when the address is a business delivery. """ company: String """Recipient first name on the shipping side.""" firstname: String! """Recipient last name on the shipping side.""" lastname: String! """Street address line on the shipping side.""" streetAddress: String! """ VAT / business id on the shipping side. Captured for B2B shipping documents. """ vatid: String """Postal code on the shipping side.""" postcode: String! """City on the shipping side.""" city: String! """ State / region on the shipping side. Used in countries that need it (e.g. US, AU). """ state: String """Country id on the shipping side. Resolves against the countries query.""" countryId: Int! """ Zone id on the shipping side. Resolves against the country's zones table. """ zoneId: Int! """Free-form delivery instructions captured at address creation.""" comments: String """ Alternate billing-side street address. Null means billing uses the shipping side. """ billingStreetAddress: String """ Alternate billing first name. Null means billing uses the shipping side. """ billingFirstname: String """Alternate billing last name.""" billingLastname: String """Alternate billing postcode.""" billingPostcode: String """Alternate billing city.""" billingCity: String """ Alternate billing country id (string-typed in this surface, vs. shipping which is int). """ billingCountryId: String """Alternate billing company name.""" billingCompany: String """Alternate billing VAT / business id.""" billingVatid: String """Alternate billing zone id.""" billingZoneId: String """Alternate billing state / region.""" billingState: String """ E-invoice operator id (Finnish OVT / Verkkolasku operator). Used when invoices are routed via an e-invoicing operator. """ billingOperatorId: Int """ E-invoice address (Finnish OVT-tunnus / EDI). Used together with billingOperatorId for e-invoice routing. """ billingInvoiceAddress: String """ Free-form reference / PO number to print on the invoice (e.g. 'PO #1234'). """ billingReference: String """ Whether the tenant flags this address as requiring a reference at checkout. true = the storefront enforces a non-empty reference before letting the order through. """ billingReferenceRequired: Boolean } """ One storefront-domain configuration row. Tenants run multi-domain setups where each domain is bound to a language (e.g. shop.example.fi for Finnish, shop.example.se for Swedish, shop.example.com for English) — this surface holds the per-domain wiring that ties one hostname to its language id, hreflang, default flag icon, and the full set of marketing-pixel ids the storefront should render in the page head. Carries: - language wiring: languageId, code, hreflang, cctld, flag, sortOrder - WordPress companion-blog link: wpDomain - per-domain AI assistant config: openaiChatInstructions / openaiVectorStoreId / openaiChatAssistantId - marketing-pixel ids: Google / Adwords / Meta / TikTok / Microsoft Ads / Pinterest / MS Clarity / Hotjar Only domains with status = 1 are returned by the resolver. The marketing-pixel and AI fields are tenant-configured per domain so one storefront's analytics / chat assistant can differ between language variants. """ type Domain { """Database row id of the domain entry.""" id: ID! """ Storefront language id this domain serves (resolves against the languages query). """ languageId: Int! """ Short language code (e.g. 'fi', 'sv', 'en'). Also the value compared by the domains(locale:) query argument. """ code: String! """ Hreflang attribute emitted in <link rel='alternate'> tags (e.g. 'fi-FI', 'en-GB'). Used by search engines to map language variants of the same content. """ hreflang: String! """Full domain name the storefront serves on (e.g. 'shop.example.fi').""" domain: String! """ Country-code top-level domain extracted from the domain (e.g. '.fi', '.se'). Used for storefront-side display and country detection. """ cctld: String! """ Filename of the flag icon shown in the language switcher (served from the tenant's flag-image directory). """ flag: String! """ Display order of the domain in the language switcher. Lower values render first. """ sortOrder: Int """ Google Analytics measurement id (e.g. 'G-XXXXXXXX'). Rendered into the storefront's analytics tag. """ googlePixel: String """ Google Analytics Measurement Protocol API secret. Used for server-side event posting. """ googleApiSecret: String """Google Ads conversion-tracking id.""" adwordsPixel: String """Google Ads conversion label for phone-call conversions.""" adwordsPhone: String """Google Ads conversion label for purchase conversions.""" adwordsPurchase: String """Google Ads conversion label for contact-form conversions.""" adwordsContact: String """Google Ads conversion label for quote-request conversions.""" adwordsQuote: String """Meta (Facebook) Pixel id.""" metaPixel: String """ Meta Conversions API access token, paired with metaPixel for server-side event posting. """ metaApiSecret: String """TikTok Pixel id.""" tiktokPixel: String """TikTok Events API access token.""" tiktokApiSecret: String """Microsoft Ads (Bing) UET tag id.""" msAdsPixel: String """Microsoft Ads goal id for phone-call conversions.""" msAdsPhone: String """Microsoft Ads goal id for contact-form conversions.""" msAdsContact: String """Microsoft Ads goal id for purchase conversions.""" msAdsPurchase: String """Microsoft Ads goal id for quote-request conversions.""" msAdsQuote: String """Pinterest Tag id.""" pinterestPixel: String """Microsoft Clarity project id (session-replay analytics).""" msClarityPixel: String """Hotjar site id.""" hotjarPixel: String """ Hostname of the WordPress companion site bound to this domain (e.g. blog / news subdomain). Null when the domain has no WP companion. """ wpDomain: String """ System-prompt / instructions for the per-domain OpenAI chat assistant rendered on the storefront. Tenants tune this to set the assistant's tone and storefront-specific context. """ openaiChatInstructions: String """ OpenAI vector-store id holding the domain-specific knowledge corpus the assistant retrieves from. """ openaiVectorStoreId: String """ OpenAI assistant id bound to this domain (for the storefront chat widget). """ openaiChatAssistantId: String } """ A delivery-address entry belonging to a customer, stored in the modern delivery_address_book table. Used by tenants who've migrated to the newer split-address schema where shipping (delivery) and billing live in separate dedicated tables — distinct from AddressBook, which holds shipping with optional alternate-billing fields in one row, and from BillingAddress, which holds standalone invoice recipients. Rows can carry a legacyAddressBookId pointing back to the original address_book row this delivery entry was migrated from; pass includeLegacy=true on the query to side-load the original AddressBook (with its billing fields) alongside the modern delivery row in the same response — useful while the migration is in flight. """ type DeliveryAddress { """Database row id of the delivery-address entry (delivery_address_id).""" id: ID! """Customer this delivery address belongs to.""" customerId: Int! """Salutation / gender code on the recipient.""" gender: String """ Company name on the recipient. Set when the address is a business delivery. """ company: String """Recipient first name.""" firstname: String! """Recipient last name.""" lastname: String! """Street address line.""" streetAddress: String! """VAT / business id captured for B2B shipping documents.""" vatid: String """Postal code.""" postcode: String! """City.""" city: String! """State / region. Used in countries that need it (US, AU, etc.).""" state: String """Country id (resolves against the countries query).""" countryId: Int! """Zone id within the country.""" zoneId: Int! """Free-form delivery instructions captured at address creation.""" comments: String """ Pointer to the legacy address_book row this delivery entry was migrated from. Null when the row was created directly in the modern delivery_address_book. """ legacyAddressBookId: Int """ Resolved legacy AddressBook (with billing fields), populated only when the query is called with includeLegacy=true and legacyAddressBookId is set. Useful while the dual-write migration is still in flight. """ legacyAddress: AddressBook } """ One day's aggregated impression and click counts for a single banner. The banners_history table stores per-event rows that the resolver groups by DATE(banners_history_date) and sums into these per-day buckets. Used to drive performance charts in the admin banner editor (line / bar chart of clicks-per-day) and to power CTR reporting. For lifetime totals see Banner.history; this surface is the day-by-day breakdown. """ type BannerHistoryEntry { """Banner the day's stats belong to.""" bannerId: Int! """Date the bucket aggregates (DATE-truncated, no time component).""" date: String! """Sum of banners_shown across the day for this banner.""" impressions: Int! """Sum of banners_clicked across the day for this banner.""" clicks: Int! } """ An incoming bank-transfer settlement matched to one order. Tenants that accept payment via invoice / bank wire reconcile their bank statement feed (typically SEPA / Finvoice for Finnish operators) into bank_payments rows so the admin's invoice tab and any ERP reconciliation report can show 'this order has been paid, on this date, in this amount'. Distinct from the gateway-specific payment surfaces (ordersNets, ordersVivawallet) — those carry the provider-side transaction state for card / mobile-wallet payments. bankPayments is for plain bank-transfer reconciliation against incoming statement lines. """ type BankPayment { """ Bank transaction id (varchar primary key, captured from the bank statement feed). """ id: ID! """Order this transfer settles.""" orderId: Int! """ Bank-statement posting date — the date the transfer appeared on the tenant's bank statement. """ invoicePaymentDate: String! """ Settlement / value date — when the funds are actually available, used for cash-flow reconciliation timing. """ invoicePaymentValueDate: String! """Amount received, in the tenant's default currency.""" amount: Float! } """ A payment notification from the Nets payment service for one order. Captured by the storefront's webhook handler (or the admin's manual reconcile flow) for every callback Nets sends — authorization callbacks, settlement notifications, refunds, and error events. The table is keyed by ordersPaytrailId (a Nets-provided session id that pre-dates the rename from Paytrail to Nets), with notificationId distinguishing successive callbacks for the same session. Use this type for reconciliation against Nets settlement reports and for diagnosing failed payments. Available only on tenants whose payment integration is Nets; other tenants will not have rows for these fields. """ type OrderNetsNotification { """ Nets-side session/order id (legacy 'paytrail' name retained for backwards compatibility). Primary key on the orders_nets table. """ ordersPaytrailId: Int! """ Customer the originating order belongs to (denormalised from orders.customers_id). """ customersId: Int! """ Sequence number distinguishing successive notifications for the same ordersPaytrailId (e.g. 1 = authorization, 2 = settlement, 3 = refund). """ notificationId: Int! """ Browser session id captured at checkout. Useful for correlating with storefront analytics. """ sessionId: String """ Nets transaction id returned by the provider. Used for reconciliation against Nets reports. """ transactionId: String """Provider-side stamp/timestamp value captured from the webhook payload.""" stamp: String """ Provider-side reference token (Nets-supplied) for this notification, used for reconciliation against settlement reports. """ reference: String """ Amount this notification covers, in the smallest currency unit (e.g. cents). May be the full order amount, a partial refund, or zero for status-only callbacks. """ amount: Float """ Error code or message from the provider when this notification represents a failed event. Null for successful events. """ error: String } """ Input shape for upsertOrdersNetsNotification — every field that Nets supplies on a callback can be persisted here. """ input OrdersNetsNotificationInput { """Nets-side session/order id (primary key).""" ordersPaytrailId: Int! """Customer the originating order belongs to.""" customersId: Int! """Sequence number for successive notifications on the same session.""" notificationId: Int! """Browser session id captured at checkout.""" sessionId: String """Nets transaction id returned by the provider.""" transactionId: String """Provider-side stamp value from the webhook.""" stamp: String """Provider-side reference token captured from the Nets webhook payload.""" reference: String """Amount in the smallest currency unit.""" amount: Float """Error code/message; pass null on successful events.""" error: String } """ A payment record from the Viva Wallet (Vivawallet) payment service for one order. Captured when the shopper completes payment through any of the Vivawallet-hosted methods (cards, Klarna, PayPal, Apple Pay, Google Pay) and returned for reconciliation against Vivawallet's merchant reports. Available only on tenants whose payment integration is Vivawallet. The paymentMethod string indicates which Vivawallet sub-method was used; transactionUuid is the cross-system identifier for chargeback and refund flows. """ type OrderVivawalletPayment { """Database row id of the payment record.""" id: ID! """Order this payment belongs to.""" orderId: Int! """Customer who placed the order (denormalised from orders.customers_id).""" customerId: Int! """ Which Vivawallet sub-method was used. Example values seen in production: - 'VivawalletKlarna' / 'vivawallet_klarna': Klarna invoice / pay-later - 'VivawalletCreditcard' / 'vivawallet_creditcard': card payment - 'VivawalletPaypal' / 'vivawallet_paypal': PayPal - 'VivawalletApplepay' / 'vivawallet_applepay': Apple Pay - 'VivawalletGooglepay' / 'vivawallet_googlepay': Google Pay Both CamelCase and snake_case variants appear in production — historical drift across module versions. Treat them as equivalent when matching by sub-method. """ paymentMethod: String! """ Sequence number distinguishing successive notifications for the same order (1 = initial, higher = retries / refunds). """ notificationId: Int! """ Vivawallet 'orderCode' identifying the payment session at the provider. Used for cross-referencing with Vivawallet's merchant portal. """ orderCode: String """ Vivawallet transaction UUID. Stable cross-system identifier — use this for chargeback handling and refund APIs. """ transactionUuid: String """ Browser session id captured at checkout. Useful for correlating with storefront analytics. """ sessionId: String } """ A country-specific address-rendering template. Different countries print postal addresses in different orders (e.g. 'CITY POSTCODE' vs 'POSTCODE CITY', whether the state goes on its own line, where the company name sits relative to the recipient name). Each Country.addressFormatId points to one of these templates, and the storefront / admin / order documents render addresses by substituting recipient values into the template. The template uses placeholders for the address fields (recipient name, street, postcode, city, state, country); the formatter consumes them when rendering an AddressBook / BillingAddress / order-shipping snapshot. """ type AddressFormat { """Database row id of the format (address_format_id).""" id: ID! """ Long-form template used to render multi-line postal addresses (the form printed on shipping labels and order documents). """ addressFormat: String! """ Short-form template used to render single-line address summaries (the form shown in lists, order tables, and dropdowns). """ addressSummary: String! } """ One audit-trail entry written by the platform's action recorder. The recorder logs sensitive admin and customer events (login attempts, password resets, export jobs, anything that needs an after-the-fact paper trail) so security and compliance reports can answer 'who did what, when, from where, and did it succeed?' against the same surface. Rows are append-only. Filter by module to scope to one event source, by userId to follow one principal across modules, and by success to reconcile failed-vs-successful attempt rates (e.g. brute-force detection). """ type ActionRecorderEntry { """Database row id of the audit entry.""" id: ID! """ Source module that wrote the row — a free-form code identifying which code path emitted the audit entry. Example values seen in production code (aicc-admin/pub/login.php, action_recorder_log.php): - 'ar_admin_login' — admin login attempt - 'ar_admin_logoff' — admin logout - 'ar_admin_force_action' — admin-triggered force operations (e.g. cache resets) Tenants may add modules; treat as free-form code. """ module: String! """ Id of the user the entry is about. Null for anonymous attempts (e.g. failed login with an unknown email). """ userId: Int """ Username / email of the user the entry is about. Captured at write time so the row stays readable even if the user is later deleted. """ userName: String """ Free-form context captured by the module — for ar_admin_login / ar_admin_logoff this holds the request IP, for ar_admin_force_action it holds the action description (e.g. 'Force Reset Redis'). """ identifier: String! """ User-Agent string of the request, captured for forensics. Null when the module didn't have request headers in scope. """ userAgent: String """ Outcome of the recorded action. true = succeeded, false = failed, null = the module wrote a status-less informational entry. """ success: Boolean """ Timestamp the entry was written. Append-only — the recorder never updates a row after creation. """ dateAdded: String! } """ A page of audit entries plus the unfiltered total, so paginated UIs can show 'showing N of M' without a second count round-trip. """ type ActionRecorderPage { """Total entries matching the filter (across all pages).""" total: Int! """The current page of entries, newest first.""" entries: [ActionRecorderEntry!]! } type CustomerPrice { id: ID! customerId: Int! productId: Int! price: Float! currency: String validFrom: String validTo: String } """ One step in a per-product step-pricing ladder. Tenants define thresholds ('buy 10 → €5.50, buy 50 → €4.90') so the storefront and quoting engine can swap in the lower unit price once the basket reaches the minimum quantity. Distinct from ProductVolumeDiscountTier — that surface applies a percentage discount keyed off customer group / customer type, while this surface replaces the unit price outright on a per-product basis. """ type ProductVolumeAddonPriceTier { """Quantity the basket must reach for this tier's price to apply.""" minimumQuantity: Int! """ Replacement unit price once the threshold is met, in the tenant's default currency. """ unitPrice: Float! } """ One tier in a group-based volume-discount ladder. The discount is expressed as a percentage off the regular price and is gated by customer group, customer type (B2B vs B2C vs all), or a specific customer id — letting tenants run wholesale ladders, contract pricing, and named-customer deals through the same surface. """ type ProductVolumeDiscountTier { """Quantity threshold the basket must reach for this discount to apply.""" minimumQuantity: Int! """ Discount percentage applied off the regular price once the threshold is met. """ discountPercentage: Float! """ Customer-group code this tier is restricted to. Null when not group-gated. """ customerGroupId: String """ Customer id this tier is restricted to. 0 (or null) means any customer. """ customerId: Int """ Customer-type gate for this tier. Example values (from the products_volume_discount_groups.customer_type enum): - 'all' — applies to all shoppers (the only value seen in production today) - 'b2b' — business shoppers only - 'b2c' — consumer shoppers only """ customerType: String } """ A saved card kept in Braintree's vault for one customer. The platform stores only the Braintree-side payment-method-token plus a few display-friendly bits (card type, masked number, expiry) — the actual PAN never lives on platform infrastructure. At checkout the storefront references the token to charge against the saved card without re-collecting card details (PCI scope reduction). One customer can have multiple saved cards (e.g. corporate + personal, or expired + replacement). The 'currently default' card isn't an explicit column on this surface — applications typically pick the most-recent dateAdded as the default, or read the default off Braintree's customer record directly. """ type CustomersBraintreeToken { """Database row id of the token entry.""" id: ID! """Customer the saved card belongs to.""" customersId: Int! """ Braintree payment-method-token (vault id). Pass this to Braintree at charge time to bill the saved card. """ braintreeToken: String! """ Card brand (e.g. 'Visa', 'MasterCard', 'Amex'). Returned by Braintree at vault time and cached here for display. """ cardType: String! """ Masked card number for display (e.g. '****-****-****-1234'). The full PAN is never stored locally — only the masked form. """ numberFiltered: String! """Card expiry, in the format Braintree returns (typically 'MM/YYYY').""" expiryDate: String! """ Timestamp the card was added to the vault. Used as a tiebreaker when picking a 'most recent' default. """ dateAdded: String! } """ A blacklist entry pinning one customer to a moderation status — typically 'blocked from purchasing' for chargeback fraud, abusive returns, or unpaid invoices. Multiple rows per customer can stack history (each new status is a fresh row, the latest wins), and the free-form reason field is kept for audit so operators can explain why a block was added later. Distinct from Customer.blockPayments / blockShipments — those are per-method bans (this customer can't pay with X / ship via Y) while CustomerBlacklist is the binary 'stop this customer from buying at all' switch. Distinct from CustomerProductInclusion — that limits which products a customer can see, while this surface gates whether they can transact in the first place. """ type CustomerBlacklistEntry { """Database row id of the blacklist entry.""" id: ID! """Customer the entry applies to.""" customerId: Int! """ Blacklist status code. Tenant-defined; typically 1 = active block, 0 = lifted / inactive. Multiple statuses stack across rows so operators can review history. """ status: Int! """ Free-form admin note explaining why the entry was added. Captured for audit. """ reason: String! """ Timestamp the entry was last edited (the resolver orders newest-first by this column). """ lastModified: String } """ An allowlist row binding one product to one customer. When a customer has any inclusion rows the catalog flips into 'private' mode for them — they can only see and order the products explicitly listed here, not the rest of the tenant's catalog. Used to run B2B private catalogs, named-account contract assortments, and confidential pre-release windows. Empty inclusions means the customer sees the standard catalog (no restriction). The check is per-customer, not per-customer-group — for group-level visibility rules see the customer-group / channel surfaces. """ type CustomerProductInclusion { """Database row id of the inclusion entry.""" id: ID! """Customer the inclusion applies to.""" customerId: Int! """Product the customer is granted access to.""" productId: Int! } input CustomerProductInclusionInput { """Customer to grant or revoke access for.""" customerId: Int! """Product to grant or revoke access to.""" productId: Int! } """ A product-specific override of a shared ProductOption. Where ProductOption defines the option globally (with its values, sort, and labels), ProductCustomOption attaches that option to one product and lets the tenant tweak per-product behaviour: whether it's required at checkout, the display position relative to other options on this product, and the input rendering type (radio / select / checkbox / textarea / etc.). This table is currently empty across production tenants — mutations are wired but no rows exist yet. The parent products_options system (rendered via productAttributes) carries the live configuration today; expect this surface to gain rows as tenants migrate to per-product option overrides. """ type ProductCustomOption { """ Database row id of the product-custom-option link (products_custom_options_id). """ id: ID! """Product this option override applies to.""" productId: Int! """Id of the parent ProductOption (products_options) being overridden.""" productsOptionsId: Int! """ Whether the shopper must pick a value for this option before adding the product to the basket. """ required: Boolean! """ Display position of this option within the product's option list. Lower values render first. """ position: Int! """ Input rendering type for this option on this product. Defaults to 'radio'. Example values (from the oscommerce-derived custom-options model used by aicc-admin/pub/custom_options.php): - 'radio' — radio-button group (default) - 'select' — dropdown - 'checkbox' — multi-select checkboxes - 'text' — short free-form input - 'textarea' — long free-form input - 'date' / 'time' / 'area' — date / time / combined date+time inputs - 'file' — file upload """ type: String! } input CreateProductCustomOptionInput { """Product to attach this option override to.""" productId: Int! """Id of the parent ProductOption being overridden.""" productsOptionsId: Int! """Whether the option is required at checkout. Defaults to false.""" required: Boolean """Display position within the product's option list.""" position: Int """ Input rendering type (see ProductCustomOption.type for example values). """ type: String } input UpdateProductCustomOptionInput { """Move the override to a different product.""" productId: Int """Point the override at a different ProductOption.""" productsOptionsId: Int """Toggle whether the option is required at checkout.""" required: Boolean """New display position within the product's option list.""" position: Int """ New input rendering type (see ProductCustomOption.type for example values). """ type: String } """ A favorited product on a customer's wishlist. Backed by the customers_favorites table — one row per (customer, product) pair. Powers the storefront's 'Heart this product' / 'Save for later' / 'My wishlist' surface, the saved-favorites widget on the customer account page, and the back-in-stock notification audience for any restock alerts a tenant runs. Distinct from customers_basket — favorites are a long-lived product list ('I want this someday'), while customers_basket is the active cart in flight to checkout. """ type CustomerFavorite { """Database row id of the favorite entry.""" id: ID! """Customer the favorite belongs to.""" customersId: Int! """Product the favorite points at.""" productsId: Int! } input AddCustomerFavoriteInput { """Customer to attach the favorite to.""" customersId: Int! """Product the customer is favoriting.""" productsId: Int! } """ Input for deleteFavorite. At least one of id, customersId, or productsId must be provided — the resolver builds an AND filter from the supplied fields and deletes the matching rows. Pass id to remove one specific favorite; pass (customersId, productsId) to remove a customer's favorite of a specific product; pass just customersId to clear the customer's wishlist; pass just productsId to remove every customer's favorite of one product. """ input RemoveCustomerFavoriteInput { """Database row id of the favorite to remove.""" id: ID """Customer scope for the removal.""" customersId: Int """Product scope for the removal.""" productsId: Int }