Appearance
MplusKassa Onboarding (Reseller)
Overview
This is the onboarding flow for a reseller (partner) bringing a merchant onto Upvendo with the MplusKassa POS. MplusKassa is a merchant-scoped, in-house POS channel commonly used in the Netherlands and Belgium: one connection covers all of the merchant's branches, and the POS owns the catalog.
Reseller onboarding is a real, code-backed flow. A merchant that a reseller onboards is stamped with reseller_id and onboarding_type = 'reseller' on the merchant record (Merchant.php), as opposed to the default 'self'. Depending on the reseller's billing model, the platform either collects subscription fees (platform_collects) or the reseller invoices the merchant directly and there is no Stripe subscription or trial (reseller_collects, Reseller.php).
Reseller portal / partner dashboard mechanics (creating the merchant, assigning it to the partner, sending login credentials, switching into "client/merchant mode", reseller invoicing) live in the reseller/partner area of the back office and are summarised under "Reseller-Specific Surface" below. The POS-side steps in MplusKassa (configuring branches/workplaces in the MplusKassa back office, registering webhook subscriptions) are not verified here — they happen in MplusKassa, not in these repos.
Guided Setup Step List (MplusKassa)
The onboarding wizard ("Guided Setup", driven by Emily) shows an ordered checklist. When Emily generates dynamic steps during discovery they are used; otherwise the wizard falls back to the built-in per-provider list. The verified MplusKassa fallback list (BASE_SETUP_STEPS.mpluskassa in src/components/dialogs/guided-setup/types.ts) is:
| # | Step | Route |
|---|---|---|
| 1 | Set up Payment Profile | /settings/payments |
| 2 | Set up Billing Profile | /settings/billing |
| 3 | Set up Store Branding | /settings/brand |
| 4 | Connect MplusKassa | /mpluskassa |
| 5 | Sync Locations | /mpluskassa |
| 6 | Complete Location Details | /settings/locations |
| 7 | Sync Menu | /mpluskassa |
Note: only Store Branding is actually enforced at connect time (see "What's enforced at connect"). Payment and billing profiles appear in the checklist but the merchant can set them up later. For a reseller on the
reseller_collectsbilling model, the merchant's subscription billing is handled by the reseller directly, so platform billing/trial prompts are suppressed (AddChannelDialog.vue).
After the base steps are complete, the wizard offers channel setup for Kiosk and/or Online Ordering — these are separate shared checklists (KIOSK_CHANNEL_STEPS, ONLINE_ORDERING_CHANNEL_STEPS), not part of the MplusKassa base list:
- Kiosk channel: Build Menu for Kiosk (
/menus/menu-builder) -> Set up In-House Settings (/in-house/settings) -> Create Device Profile (/device-management/profiles) -> Set up Kiosk & Activate Payment (/device-management/devices) -> Activate Kiosk & Test Order. - Online Ordering channel: Build Menu for Online Ordering (
/menus/menu-builder) -> Set up Online Settings (/online-settings) -> Set up Online Ordering (/online/online-ordering) -> Open Online Ordering URL & Test Order.
(See the Guided Setup feature doc for how completion is auto-detected from the merchant's real data via GET /api/back-office/setup-status.)
Integration Mechanics
Merchant-Scoped, Single In-House POS
MplusKassa is merchant-scoped: one connection covers all branches, and the merchant's MplusKassa branches are imported as Upvendo locations on connect/sync.
A merchant may have only one in-house POS channel at a time. MplusKassa is one of the in-house POS channels (alongside Square, Lightspeed, Hendrickx, Vanhoutte, Shopcaisse). The backend hard-blocks connecting MplusKassa if another in-house channel already exists for the merchant — ThirdPartyIntegrationHelper::checkForInhouseChannel aborts with HTTP 400 ("Another in-house channel is already enabled for this merchant"), and the connect dialog disables other in-house channels with reason merchant_has_pos (OrderingChannelService::inHouseChannels, AddInhouseChannelDialog.vue). This is the real "in-house exclusivity" rule: it is mutual exclusivity among in-house POS channels, not POS-vs-in-house.
MplusKassa Owns the Catalog (Read-Only Sync)
The menu/catalog sync is one-directional: MplusKassa -> Upvendo. The synced fields are locked in Upvendo so they cannot drift:
- Items:
name,description,price,plu,modifier_group_ids,allergens,dietary_preferencesare locked (Item.phpgetUneditableFields, enforced inItemRepository::sanitizeData). - Categories:
name,descriptionare locked (Category.php). - Modifier groups:
name,is_mandatory,allow_select_more_than_one,allow_same_modifier_more_than_one,min_selected,modifier_ids,pricing,modifier_pricesare locked (ModifierGroup.phpgetUneditableFields). Note:max_selected("Maximum of modifiers a customer can select") is left editable for MplusKassa groups and syncs back to MplusKassa — it is the one modifier-group field a merchant can change in Upvendo.
Fields that do not come from MplusKassa (item images, availability/visibility, channel settings) stay editable.
There is no production bulk push-back of the catalog from Upvendo to MplusKassa: the catalog write APIs exist only on a developer/test route (routes/api/dev/mpluskassa.php, loaded only in the test environment). One narrow exception is real in production: modifier-group edits (preparation-method / article-alteration groups) are pushed/updated/deleted back to MplusKassa by SyncMplusKassaMenuJob when a menu entity changes (IntegrationTrait::syncToThirdParty -> SyncMplusKassaMenuJob). Items and categories are never pushed back.
Pull Mapping (MplusKassa -> Upvendo)
The import maps:
| MplusKassa | Upvendo | Notes |
|---|---|---|
| ArticleGroup (TurnoverGroup fallback) | Category | Source of categories; saveArticleGroupAsCategory / syncTurnoverGroupsAsCategories |
| Article | Item | The sellable item; external id = articleNumber |
| PreparationMethodGroup | ModifierGroup | Pull import (push-back exception above) |
| ArticleAlterationsGroup | ModifierGroup | Pull import (push-back exception above) |
| Branch | Location | Auto-imported on connect/sync |
| VAT code (per article) | Tax rate | Auto-created as MPLUS_VAT_{code}, tax method inclusive |
Stock (amountFree) | Inventory | Pull only |
Modifier-group external IDs are stored as a composite string {type}_{number} (e.g. preparation_method_5, article_alteration_10); the number is extracted via MplusKassaTrait::extractGroupNumberFromCompositeId and is never cast directly to an integer.
Actions
Connect MplusKassa
Route: /mpluskassa
Enter the MplusKassa API connection details and click Connect. The connect request (InitiateMplusKassaRequest, posted from MplusKassaForm.vue / MplusKassaChannelDialog.vue) carries these fields:
- API ident -- payload key
ident(required at connect) - API secret -- payload key
secret(required at connect) - API URL -- payload key
api_url(optional; defaults to the standard MplusKassa endpoint) - API port -- payload key
api_port(optional integer)
The Connect button is disabled until ident and secret are entered. On connect, Upvendo imports the merchant's MplusKassa branches as locations.
What's enforced at connect
Only the branding profile is enforced. MplusKassaIntegrationService::initiate calls ThirdPartyIntegrationHelper::checkIntegrationPrerequisites, which (in checkRequiredProfiles) requires at least one branding profile for MplusKassa and aborts with HTTP 400 ("Missing required profiles") if none exists. Payment and billing profiles are explicitly optional at connect ("Payment and billing profiles are now optional - merchant can set them up later") — the merchant can add them afterwards.
Configure Branch and Workplaces
Once connected, choose the branch and the workplaces that orders are injected into (InitiateMplusKassaRequest):
- Branch -- payload key
branch_number - Online ordering workplace -- payload key
online_workplace_number; where Online Ordering orders are sent - Table QR workplace -- payload key
qr_workplace_number; optional (clearable in the UI). When not set, Table QR orders use the online workplace (the UI states "Table QR orders will use the online workplace").
Sending different channels to different workplaces improves POS reporting.
Set the Daily Sync Time
Set a sync time for the automatic daily catalog/inventory sync. The time is restricted to a whole or half hour — minutes must be 00 or 30 (validated server-side by the regex ^([01]?[0-9]|2[0-3]):(00|30)$ in InitiateMplusKassaRequest; the form also snaps any other minute value to 00/30). The value is stored as the merchant's local wall-clock time, not converted to UTC, so a 03:30 sync stays at 03:30 across daylight-saving changes. The form displays the resolved timezone as a caption under the field; it is not a merchant-selectable option.
Sync Now
Triggering a sync pulls new and changed products/articles from MplusKassa using sync markers, maps ArticleGroups (TurnoverGroups fallback) to Categories and Articles to Items, downloads images, imports preparation-method / article-alteration groups as modifier groups, and updates VAT rates and stock. The sync refuses to start if one is already in progress.
Complete Location Setup
After branches import, go to Settings -> Locations (/settings/locations) to complete fields the POS does not provide (address coordinates, business hours, delivery zones, etc.). Imported locations are assigned the default branding profile (required); the default billing/payment profiles are assigned when present (optional, non-fatal).
Disconnect (administratively disabled)
Disconnecting is administratively disabled. The back office still surfaces a disconnect option, but POST /mpluskassa/disable short-circuits in MplusKassaIntegrationController::disable and returns HTTP 400 with: "Disconnecting MplusKassa integration is disabled. Please contact administrator." The teardown logic in the service is never reached through this endpoint.
Reseller-Specific Surface
These reseller behaviours are grounded in code (backend app/RawModels/Reseller.php, app/Services/BackOffice/ResellerService.php, app/Services/Subscription/ResellerSubscriptionService.php, routes/api/backoffice/resellers.php; backoffice resellers/, clients/, partner pages):
- Roles: reseller users have
reseller-owner/reseller-administrator/reseller-reproles (app/Constants/Roles.php). The top global role isglobal-administrator(not "super-admin"). - Creating a merchant:
POST /partner/merchants -> ResellerController::createMerchant(authorised for global users) creates the merchant + owner user, stampsreseller_idandonboarding_type = 'reseller', and appends the merchant to the partner user'svendor_ids.POST /partner/merchants/{merchantId}/send-credentialsresets the owner password (setsmust_change_password) and emails credentials. - Managing multiple merchants: there is no "impersonate / login as" feature. Instead the partner/admin uses client/merchant mode switching (
switchMerchant, areseller_idJWT claim viaAuthService) plus a partner dashboard (/dashboard/partner,getPartnerMerchants) and reseller-detail tabs (Users, Merchants assign/unassign, Subscriptions/Invoices, Payouts). - Billing model drives subscription handling (
Reseller.php):platform_collects(platform bills, with a reseller-onboarding grace period) vsreseller_collects(reseller invoices the merchant directly — no Stripe subscription, no trial;ResellerSubscriptionService). Onreseller_collects, the onboarding channel dialog hides the free-trial badge (AddChannelDialog.vue). - Reseller's POS provider set:
GET /merchant/reseller-providers -> getMerchantResellerProvidersreturns only the reseller's allowed POS providers (reseller->getPosProviders()); the auth payload exposesreseller_providersandbilling_model. This is what scopes which POS options the reseller's merchant sees during onboarding.
Business Rules
- MplusKassa is the source of truth for the catalog, prices, VAT and stock; the synced fields are locked in Upvendo.
- The menu sync is one way (MplusKassa -> Upvendo) for items and categories. The only production write-back is modifier groups (preparation-method / article-alteration), pushed by
SyncMplusKassaMenuJob. - Inventory is one way (MplusKassa -> Upvendo): the sync reads
amountFreeper article per branch. - Branch-specific price deviations are honoured: a branch's article price overrides the default for that location.
- A merchant may have only one in-house POS channel; connecting MplusKassa is blocked (HTTP 400) if another in-house channel exists.
- Only the branding profile is enforced at connect; payment and billing profiles are optional and can be added later.
- Connect requires
identandsecret;api_urlandapi_portare optional. - Sync time must be a whole or half hour (
:00or:30), and is interpreted as the merchant's local time (stored as a local wall-clock, so it does not drift with daylight saving). - Disconnect is administratively disabled (HTTP 400, "contact administrator").
- Orders flow one way (Upvendo -> MplusKassa) and are forwarded automatically once an order reaches a syncable status; there is no manual-acceptance hold. VAT method sent to MplusKassa is
INCLUSIVE. (See the shared MplusKassa integration doc for the full order-forwarding, webhook, external-payment and relation-pricing detail.) - Reseller-onboarded merchants carry
reseller_id+onboarding_type = 'reseller'; on thereseller_collectsbilling model there is no platform Stripe subscription or trial.
Prerequisites
Surfaced in the guided-setup checklist (only branding is enforced at connect):
Branding Profile (Required at connect)
- Go to Settings -> Brand (
/settings/brand). - Configure the store's branding (logo, colours, etc.).
- The default branding profile is assigned to imported locations. Connect is blocked (HTTP 400) if no branding profile exists.
Payment Profile (Optional — needed to accept customer payments)
- Go to Settings -> Payments (
/settings/payments). - Set up payment providers (e.g. Stripe for Online Ordering, Viva Wallet for Kiosk terminals).
- Configure payment methods (iDEAL is common in the Netherlands; cards; Bancontact for Belgian customers).
Billing Profile (Optional)
- Go to Settings -> Billing (
/settings/billing). - Complete the billing information. On a
reseller_collectsreseller, the reseller invoices the merchant directly, so platform billing is handled by the reseller.
Netherlands / Belgium Notes
Payment Methods
Common choices for Dutch and Belgian customers:
- iDEAL -- popular in the Netherlands (via Stripe)
- Card -- Visa, Mastercard
- Bancontact -- for Belgian customers
VAT (BTW)
There is no tax-mapping screen. Each MplusKassa article carries its own VAT code/percentage, and Upvendo auto-creates a matching tax rate (MPLUS_VAT_{code}) with the inclusive tax method on import. Branches can be in the Netherlands or Belgium; country, timezone and language are derived from the MplusKassa branch.
FAQs
Q: Can the merchant edit menu items or prices in Upvendo? A: Not for fields that come from MplusKassa. MplusKassa owns the catalog, so item name, description, price, PLU, modifier groups, allergens and dietary preferences (and category name/description) are locked. Make those changes in MplusKassa, then sync.
Q: Is the menu sync bidirectional? A: No for items and categories — it is one way, MplusKassa -> Upvendo. The only production write-back is modifier groups (preparation-method / article-alteration), pushed by a background job when a menu entity changes.
Q: What does the reseller actually need before connecting MplusKassa? A: A branding profile (the only prerequisite enforced at connect). Payment and billing profiles can be set up later.
Q: Can a merchant have MplusKassa and another POS at the same time? A: No. Only one in-house POS channel per merchant; connecting MplusKassa is blocked if another in-house channel is already enabled.
Q: How does a reseller create the merchant? A: Through the partner area (POST /partner/merchants), which creates the merchant and owner user, stamps it with the reseller ID, and can email login credentials. Managing the merchant afterwards uses client/merchant-mode switching, not impersonation.
Q: Who bills the merchant? A: It depends on the reseller's billing model. platform_collects = the platform bills the merchant (with a reseller-onboarding grace period). reseller_collects = the reseller invoices the merchant directly, with no platform Stripe subscription and no trial.
Q: Can I disconnect MplusKassa from the back office? A: No. The disconnect option is administratively disabled and returns an error asking you to contact an administrator.
Troubleshooting
Connect is rejected
- "Missing required profiles" -> create a branding profile first (Settings -> Brand). Branding is the only profile enforced at connect.
- "Another in-house channel is already enabled for this merchant" -> the merchant already has an in-house POS channel; only one is allowed.
- Connect button stays disabled -> enter both API ident (
ident) and API secret (secret).
Locations not imported
- Check that the MplusKassa account has active branches.
- After import, complete the missing fields in Settings -> Locations.
- If a branch was added after the initial connection, run a sync to detect it.
Menu/price changes not appearing
- Make the change in MplusKassa, then sync (the field is locked in Upvendo).
- Confirm the sync ran (check logs / sync markers); sync won't start if one is already in progress.
Sync time won't save
- The sync time must be a whole or half hour (minutes
00or30). Other values are rejected (or snapped to the nearest in the form).
Can't disconnect
- Disconnect is administratively disabled. Contact an administrator.