@feedmepos/mf-upselling

中文文档
0.3.2发布于 2026/10/29UNLICENSEDTypeScriptES Module零依赖近 30 天下载 2126

The merchant settings microfrontend for Connect. All feature UI lives here; the API runs in report-query-service under packages/node. Connect imports the UI and supplies its existing authenticated context. No new service or menu editor.

原始使用文档
文档目录6 节
  1. What this release does
  2. Core / Connect integration
  3. Delivery
  4. API / collection
  5. Build and test
  6. Portal regional API integration

Upselling settings

The merchant settings microfrontend for Connect. All feature UI lives here; the API runs in report-query-service under packages/node. Connect imports the UI and supplies its existing authenticated context. No new service or menu editor.

What this release does

  • Loads existing products for the selected business or restaurant; shows names, photos, category and price without copying them into MongoDB.
  • A product's Adjust weight switch controls whether its saved weight applies. OFF is normal ranking, not exclusion.
  • First activation starts at 50%. Switching OFF retains the value. Active 0% excludes; active 100% is the strongest preference, never guaranteed placement or a purchase probability.
  • Drag freely in 1% increments; releasing gently snaps to the nearest 5%. Labels are Do not recommend at 0%, Weak at 1–39%, Normal at 40–59%, Strong at 60–79%, and Strongest recommendation at 80–100%. The orange/gold flowing effect starts at 80% and respects reduced-motion preferences. These are weight labels, not model scores.
  • Stores only configured products. Search never changes what is saved. Products absent from the current catalog remain visible as unavailable, with their saved settings retained.
  • Failed saves retain edits. Switching restaurants preserves unsaved drafts for this mounted session. Requests and late responses remain bound to their original scope. No draft is stored in localStorage.
  • Customer preview selects an outlet and either restaurant or cart recommendations. The test cart supports multiple products and quantities, uses that outlet’s catalog, and is cleared when the outlet changes. No menu, order or settings writes are made by preview controls.

The preview compares Before (current) with After using the same outlet, recommendation type and cart. Optional loadPreview(request): Promise<UpsellPreviewResult> accepts { businessId, restaurantId, recommendationType: 'restaurant' | 'cart', cart: { productId, quantity }[], items: UpsellItem[], limit: 10 } and returns { products: UpsellProduct[], source: 'historical' | 'weight_only' | 'no_data' }. Before uses the selected outlet’s effective saved settings; After overlays only matching unsaved changes, preserving unrelated outlet weights. Provider order is retained. Weight-only suggestions are labelled as such, not as top-selling products. Without a real provider the panels say preview is not connected; no client-side ranking is invented. previewTouchpoints may customize titles for restaurant/cart; an empty array hides the preview. Preview context is not persisted. Draft-only weight edits reuse the session’s saved Before result and loaded outlet catalog; only After is requested again. Changing outlet, recommendation type, cart, saved settings or retrying refreshes the baseline. An already open preview does not poll for other users’ saved changes. Selecting a cart product adds it immediately; compact rows expose quantity and removal. While refreshing the same recommendation context, existing results stay visible and keyed products animate to their new positions; reduced-motion preferences disable those transitions. Switching the outlet or recommendation type clears results from the previous context.

createUpsellRecommendationPreview({ baseUrl, getToken, loadProducts }) uses Eric's dedicated POST /recommendation/:businessId/current for Before and /recommendation/:businessId/preview for After. Requests use { type: 'whats_hot' | 'cart', restaurant_id, cart_item?: string[] }; preview additionally sends the complete items array (empty means no boosts). Both send the existing Portal bearer token and the same explicit outlet/cart. Current omits draft items and accepts settings source: restaurant | business | none. Both require matching restaurant_id, ordered unique product_id values and finite aligned score values. The API does not expose historical-relevance metadata: the adapter returns source: unknown and hides relevance/weight badges rather than inventing status. Recommendation calls directly report endpoint failures; no separate health request is made. Failures are surfaced, never converted to no-history suggestions. The frontend preserves server order and customer eligibility/display constraints. This local integration does not establish deployed availability or authenticated end-to-end behavior.

Merchant controls use FeedMe FmSelect and a compact two-option recommendation selector. The read-only customer tiles follow ordering-service's customer UpsellingItemList, ItemListTile and checkout placement: horizontal photo/name/price tiles with no artificial Add button. They illustrate the default layout; merchant ordering themes, product codes, descriptions, live availability and ordering actions are not reproduced or fetched by this package.

Each customer viewport is capped at 375px and shrinks to the space available. Up to ten returned products fit in a two-column grid with proportional square photos and wrapped names, without horizontal scrolling. The settings layout responds to its host container width, including space taken by Connect's sidebar.

Core / Connect integration

Delivery

The next MY dev candidate is 0.3.0-dev.1. After this PR is merged and its mf-upselling/0.3.0-dev.1 workflow succeeds, install @feedmepos/[email protected] from the public npm registry. This candidate publishes to dev; stable latest remains unchanged. A package PR or branch push does not publish or deploy anything.

Install the stable version only after its matching release workflow succeeds:

pnpm add --save-exact @feedmepos/[email protected]

This package is delivered through npmjs, not GitHub Packages. If your local @feedmepos scope points at GitHub Packages, use the host's npmjs package-resolution setup rather than changing its other private dependencies.

For Portal import-map delivery, register that exact version's dist/app.js and load dist/style.css; the package does not register itself. Use the host's shared Vue, Vue Router and FeedMe UI. Do not use an unpinned latest URL.

MY API bases (after the upselling gateway route is deployed): dev https://query-engine.feedmeapidev.com, production https://query-engine.feedmeapi.com. Do not append /report-query for upselling. With only businessId, the client uses /{businessId}/upsell-settings for GET and PUT. Supplying restaurantId uses the existing /{businessId}/{restaurantId}/upsell-settings. Other countries must provide their matching gateway URL. The backend is deployed separately, not included in the npm package; production settings remain unavailable while UPSELL_SETTINGS_ENABLED=false.

The Upselling package workflow checks package PRs. After merge, mf-upselling/<version> must match the package version: stable versions publish to latest, and -dev.N versions publish to dev. Dev releases require an existing stable latest and verify it stays unchanged. Release commits must be contained in main. Publishing does not deploy the backend; Connect supplies the API URL and authentication for its environment.

Connect's packages/connect/src/api/index.ts normally targets marketing services and handles its code/data envelope. Do not reuse that client unchanged for report-service: the settings endpoint returns a plain settings object. Supply a report-service client with the correct country/environment base URL and a current Portal Firebase token. Host context comes from useCoreStore(); this package deliberately does not create another login session.

The host must install FeedMeUI and load @feedmepos/ui-library/styles.css. Load this package's stylesheet once:

import { createUpsellSettingsApi, createUpsellingRoutes } from '@feedmepos/mf-upselling'
import '@feedmepos/mf-upselling/style.css'

// An authenticated Axios-compatible client pointed at report-query-service,
// not Portal's default portal-v2 API. Read the current token per request;
// keep the base URL aligned with the selected country and environment.
const api = createUpsellSettingsApi(reportServiceClient)

const upsellRoutes = createUpsellingRoutes(() => ({
  businessId: selectedBusinessId.value,
  // Omit restaurantId for business defaults; pass it only when editing an override.
  restaurantName: selectedRestaurantName.value,
  api,
  loadProducts: loadExistingRestaurantProducts,
}))
// Include upsellRoutes in Connect's child routes. The relative path is 'upselling'.

reportServiceClient, selected IDs and loadExistingRestaurantProducts above are host-owned dependencies, not new package globals. Keep the client and product loader references stable. Recreate the client (or remount the component) when country, environment or signed-in user changes; do not reuse a mounted draft session across users.

loadProducts({ businessId, restaurantId }) must resolve the complete product list for that scope, paging the existing source if needed. Without restaurantId, return the business catalog; do not require a restaurant to exist or substitute an arbitrary one. With it, return that restaurant's products. Return a plain array, not an Axios response or { data, items, total } envelope. Unwrap and map the existing catalog response in the host adapter; raw Connect catalog compatibility is not assumed. Each productId must be unique in the returned array. Map each row into:

interface UpsellProduct {
  productId: string // exact identity used in recommendation product_id[]
  name: string
  imageUrl?: string
  category?: string
  priceLabel?: string // already formatted by the existing menu source
}

// Exported contract: UpsellingProps['loadProducts']
type LoadProducts = (scope: {
  businessId: string
  restaurantId?: string
}) => Promise<UpsellProduct[]>

Do not substitute a row index, variant ID or menu item ID unless it is genuinely the recommendation engine's product identity. This adapter deliberately avoids coupling the package to a specific menu schema/version. A missing or broken photo shows text, not an icon.

Alternatively, mount <UpsellingSettings> with the same props. It emits dirty-change, busy-change and saved. Direct component hosts must guard navigation using those events; the route factory includes the unsaved-change confirmation and prevents navigation during a save. Both modes warn on browser unload. Translate by supplying the messages prop; i18nMessages exports the upselling namespace with English defaults for the host to merge.

Optional loadApplyTargets(businessId): Promise<UpsellApplyTarget[]> enables an Editing scope selector above Products. Each target supplies { scope: { businessId, restaurantId? }, name, products, unavailableReason? }, with the host resolving its actual effective catalog using menu-service's menu-mode, catalog-assignment and v3/v4 membership rules. Omit restaurantId for business defaults. Never flatten independent outlet catalogs or match products by name. Unavailable outlet targets are excluded; scope loading failures disable Apply and offer a retry. Without this callback, existing single-scope Save behavior is unchanged.

The scope selector, editor and preview share one catalog load for the mounted business; changing the business or catalog provider invalidates it, and failed loads can be retried. Selecting business defaults shows the business catalog and retains a separate Preview outlet selector. Selecting an outlet shows only that outlet’s available products, loads its effective settings, and pins preview to that outlet. Inherited settings are identified explicitly. Apply saves directly to the selected scope without a destination dialog. Saved settings for products outside the visible catalog are retained; business-default updates never remove outlet overrides. Drafts are restored independently per business/outlet for the mounted session, and navigation warnings include drafts in other scopes. Saving or discarding affects only the selected draft. Existing GET/PUT remains last-write-wins, without atomic concurrency protection. Business defaults require a host-verified shared catalog. No bulk API, schema or index change is introduced.

API / collection

Backend flag: UPSELL_SETTINGS_ENABLED=true explicitly enables settings. Unset or any other value disables the feature: no upselling Mongo connection/service is registered, and authenticated settings requests return 503. Other report APIs are unchanged. Restart/redeploy to change the flag; enable only after the database and index are provisioned.

Public GET and PUT /:businessId/upsell-settings edit business defaults without requiring a restaurant. The existing /:businessId/:restaurantId/upsell-settings route edits a restaurant override; the gateway rewrites it to internal /:businessId/restaurants/:restaurantId/upsell-settings. All routes require the existing Firebase bearer token. There is no feature-level edit permission. Business members (even with no feature permissions) and callers with existing owner/agent/admin business access may edit. Restaurant-scoped requests still check that the restaurant belongs to that business.

A saved restaurant document overrides the entire business configuration, including when its items is explicitly empty. Without one, restaurant GET returns the business defaults with the requested restaurantId and derived inherited: true; it creates no copy. Saving inherited edits creates a restaurant override. DELETE on the restaurant route removes that override and returns the current defaults. DELETE on the business route is rejected. The UI offers “Use business defaults” when the API adapter provides reset(); the fetch adapter supports it, and Axios hosts must supply delete() for this control. Do not merge individual products from the two scopes. The recommendation engine must resolve the same restaurant-then-business fallback before applying scores; that integration remains with Eric and is not changed by this settings release.

GET returns { businessId, items: [], updatedAt: null } when business defaults have not been saved, without creating a document. Restaurant responses include the requested restaurantId and inherited: true when no override exists. PUT accepts only { items: [...] } and replaces the configured list atomically. Both return the settings; saved responses also have _id and an ISO updatedAt. PUT rejects unknown fields, duplicate product IDs, non-boolean switches and non-finite/out-of-range weights. Technical limit: 10,000 configured products per scope; product IDs are non-blank strings up to 256 characters.

dataDB.upsellSettings {
  _id,
  businessId,
  restaurantId: null | restaurantId, // null for the business default
  items: [
    {
      productId,
      adjustWeight: Boolean,
      weight: Number // 0..1
    }
  ],
  updatedAt: Date
}

Provision the unique index on { businessId: 1, restaurantId: 1 }, named upsellSettings_business_restaurant, once before enabling writes. The application does not create indexes at startup. Confirm the index exists on the actual deployment database; it prevents duplicate settings during concurrent first saves. The named upsellDataDB connection uses the existing MONGODB_URL, explicitly selecting dataDB; existing companyDB repositories remain unchanged. Runtime credentials need read/write access to the collection, but do not need index-creation permission for this feature. No data migration/backfill is needed for this new collection. Concurrent editors use last successful save wins; there are no version fields.

Build and test

pnpm install --frozen-lockfile
pnpm test
pnpm build
pnpm check:package
pnpm build:example
pnpm test:backend

test:backend runs targeted Nest HTTP/MongoDB tests against a disposable loopback MongoDB, then stops it. It downloads the official MongoDB binary on first use. Firebase verification is stubbed; no application database credentials are used.

For unit-only backend tests, run yarn test --runInBand --testPathPattern=upsell-settings in packages/node; live Mongo tests skip unless the disposable harness supplies its URI.

The host example in example/main.ts requires window.upsellingHost with apiBaseUrl, businessId, optional restaurantId and restaurantName, getToken() and loadProducts(scope). Resolve the current Portal Firebase token per request; never place tokens in Vite environment variables or browser storage. Without host context, the example makes no requests.

createUpsellFetchApi({ baseUrl, getToken }) is exported for hosts without an Axios client. It uses the same plain-response GET/PUT API with no mock fallback. The backend must allow the host origin through its CORS configuration.

The settings API and recommendation API must read the same saved-settings dataset. A local recommendation host with isolated Mongo needs an explicit test-business snapshot matching the editor's settings reads; an empty local database is not a valid comparison baseline.

Business-default previews preserve an outlet's whole override, matching Apply; drafts only overlay inherited defaults or the outlet being edited. Preview requests reject non-local HTTP URLs and redirects, and bound recommendation calls to 15 seconds. Storage permission/outage/configuration failures return errors; only a genuine missing model permits cold-start fallback. Missing models share a bounded 30-second cache to avoid repeated storage reads; newly generated history can take up to 30 seconds to appear. Storage errors are not cached.

Eric's exact outlet-specific missing-model 404 is displayed as an empty preview, so an empty Before does not hide a weighted After. Other 404s and service failures remain errors.

Portal regional API integration

createUpsellPortalApi(coreStore, { loadProducts }) owns settings and recommendation requests. It reads Portal's reactive namespace, currentCountry and sessionUser.token; Connect does not build the API hosts. Use getUpsellingEndpoints(country, namespace) to gate navigation and rendering. The factory resolves the selected region and a fresh token for each call.

Configured production countries: MY, SG, TH, VN, AE, AU. Dev also includes JP and PH. MY has no country subdomain; other countries use the lowercase country code: query-engine.sg.feedmeapi.com and public-query-engine.sg.feedmeapi.com. Dev uses feedmeapidev.com. Unsupported countries/namespaces are rejected without falling back to MY. Regional probes establish routing/authentication responses, not authorized rankings or regional capacity.

版本与发布记录

@feedmepos/mf-upselling 共发布 12 个版本,首次发布于 2026/10/14;过去 12 个月发布了 12 个版本。

版本发布日期距今
0.3.2latest2026/10/2916 天前
0.3.12026/10/2916 天前
0.3.02026/10/2817 天前
0.3.0-dev.2dev2026/10/2718 天前
0.3.0-dev.12026/10/2718 天前
0.2.22026/10/2025 天前

关于 @feedmepos/mf-upselling 的常见问题

@feedmepos/mf-upselling 是什么?

The merchant settings microfrontend for Connect. All feature UI lives here; the API runs in report-query-service under packages/node. Connect imports the UI and supplies its existing authenticated context. No new service or menu editor.

如何安装 @feedmepos/mf-upselling?

在项目目录运行 npm install @feedmepos/mf-upselling,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:vue(^3.4.21)、vue-router(^4.3.0)、@feedmepos/ui-library(^1.8.8)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@feedmepos/mf-upselling 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@feedmepos/mf-upselling 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@feedmepos/mf-upselling 可以商用吗?

@feedmepos/mf-upselling 使用 UNLICENSED 许可。作者声明为 UNLICENSED,即不授予公开使用许可(与公共领域的 Unlicense 不同)。使用前需获得作者授权。以上为概括,不构成法律意见,请以许可原文为准。

@feedmepos/mf-upselling 的下载量有多少?

根据 npm 官方下载统计,@feedmepos/mf-upselling 在 2026/10/15 – 2026/11/13 这 30 天内被下载 2,126 次,日均约 71 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@feedmepos/mf-upselling 还在维护吗?

最新版本 0.3.2 发布于 2026/10/29(16 天前);至今共发布 12 个版本,其中过去 12 个月发布了 12 个;首次发布于 2026/10/14。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。