From 2b5ce6fd8b92af38471575f4283e3212baea0d9a Mon Sep 17 00:00:00 2001 From: Marvin Besselsen Date: Thu, 11 Jun 2026 09:27:01 +0200 Subject: [PATCH 1/2] Fix customer assigned to wrong store view on order import Set storeId on newly created customers during Channable order import. Previously only websiteId was set, causing customers to default to the first store view of the website instead of the intended store view. --- Service/Order/Quote/CustomerHandler.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Service/Order/Quote/CustomerHandler.php b/Service/Order/Quote/CustomerHandler.php index 3c166270..3abb563d 100644 --- a/Service/Order/Quote/CustomerHandler.php +++ b/Service/Order/Quote/CustomerHandler.php @@ -92,6 +92,7 @@ public function assignCustomer(Quote $quote, array $orderData) } catch (NoSuchEntityException $exception) { $customer = $this->customerFactory->create(); $customer->setWebsiteId($websiteId); + $customer->setStoreId((int)$storeId); $customer->setFirstname($this->validateName($orderData['customer']['first_name'], 'first_name')); $customer->setMiddlename($this->validateName($orderData['customer']['middle_name'], 'middle_name')); $customer->setLastname($this->validateName($orderData['customer']['last_name'], 'last_name')); From f9003187bf4e5e9a2069734a294a8d7b1dd68e3d Mon Sep 17 00:00:00 2001 From: Marvin Besselsen Date: Thu, 11 Jun 2026 10:55:05 +0200 Subject: [PATCH 2/2] Add E2E test for customer store_id assignment on order import Also stop feed tests from overwriting the Channable token in Magento config, which broke all subsequent order/webhook tests with Invalid token. --- .../support/services/ChannableApi.ts | 85 +++++++++++++ .../tests/feed/feed-generation.spec.ts | 119 ++++++++++++++++++ .../tests/order/order-import.spec.ts | 37 +++++- 3 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 Test/End-2-end/tests/feed/feed-generation.spec.ts diff --git a/Test/End-2-end/support/services/ChannableApi.ts b/Test/End-2-end/support/services/ChannableApi.ts index 29ad8753..5abea165 100644 --- a/Test/End-2-end/support/services/ChannableApi.ts +++ b/Test/End-2-end/support/services/ChannableApi.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; +import { execSync } from 'child_process'; import BaseApi from './BaseApi'; export default class ChannableApi extends BaseApi { @@ -63,6 +64,31 @@ export default class ChannableApi extends BaseApi { return response.json(); } + /** + * GET a customer by email via the Magento REST API. + */ + async getCustomerByEmail(baseURL: string, email: string): Promise { + const token = process.env.admin_token; + const searchUrl = `${baseURL}rest/V1/customers/search?` + + `searchCriteria[filterGroups][0][filters][0][field]=email&` + + `searchCriteria[filterGroups][0][filters][0][value]=${encodeURIComponent(email)}`; + + const response = await fetch(searchUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json', + }, + }); + + const result = await response.json(); + if (result.items && result.items.length > 0) { + return result.items[0]; + } + + throw new Error(`Customer not found for email: ${email}`); + } + /** * Build order data by merging overrides into the base template. */ @@ -84,6 +110,7 @@ export default class ChannableApi extends BaseApi { companyName?: string; channelName?: string; shipmentMethod?: string; + email?: string; } = {}): any { const channableId = overrides.channableId || String(Math.floor(Math.random() * 900000) + 100000); const country = overrides.country || 'NL'; @@ -148,6 +175,12 @@ export default class ChannableApi extends BaseApi { data.shipping.company = overrides.companyName; } + if (overrides.email) { + data.customer.email = overrides.email; + data.billing.email = overrides.email; + data.shipping.email = overrides.email; + } + if (overrides.channelName) { data.channel_name = overrides.channelName; } @@ -169,6 +202,58 @@ export default class ChannableApi extends BaseApi { return data; } + /** + * Ensure a second store view exists for multi-store tests. + * Uses Magento bootstrap to create the store properly (triggers all observers/indexers). + * Returns the store ID, or null if no container is available. + */ + ensureSecondStoreView(storeCode: string): number | null { + if (!this.container) return null; + + const phpScript = [ + 'getObjectManager();', + '$repo = $om->get(\\Magento\\Store\\Api\\StoreRepositoryInterface::class);', + 'try {', + ` $store = $repo->get('${storeCode}');`, + '} catch (\\Magento\\Framework\\Exception\\NoSuchEntityException $e) {', + ' $store = $om->get(\\Magento\\Store\\Model\\StoreFactory::class)->create();', + ` $store->setCode('${storeCode}');`, + " $store->setName('Second Store');", + ' $store->setWebsiteId(1);', + ' $store->setGroupId(1);', + ' $store->setIsActive(1);', + ' $store->setSortOrder(10);', + ' $store->save();', + '}', + 'echo $store->getId();', + ].join('\n'); + + const tmpFile = '/tmp/e2e-create-store.php'; + execSync(`docker exec -i ${this.container} tee ${tmpFile}`, { + input: phpScript, + stdio: ['pipe', 'pipe', 'pipe'], + }); + + const result = execSync( + `docker exec ${this.container} php ${tmpFile}`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 120000 } + ).trim(); + + const storeId = parseInt(result, 10); + + execSync(`docker exec ${this.container} bin/magento config:set --scope=stores --scope-code=${storeCode} magmodules_channable/general/enable 1`, { stdio: 'pipe' }); + execSync(`docker exec ${this.container} bin/magento config:set --scope=stores --scope-code=${storeCode} magmodules_channable_marketplace/general/enable 1`, { stdio: 'pipe' }); + execSync(`docker exec ${this.container} bin/magento indexer:reindex`, { stdio: 'pipe', timeout: 120000 }); + this.flushAllCaches(); + console.log(`Second store view '${storeCode}' ensured (ID: ${storeId}).`); + + return storeId; + } + /** * Ensure a currency rate exists in Magento (needed for multi-currency orders). */ diff --git a/Test/End-2-end/tests/feed/feed-generation.spec.ts b/Test/End-2-end/tests/feed/feed-generation.spec.ts new file mode 100644 index 00000000..1716a7f1 --- /dev/null +++ b/Test/End-2-end/tests/feed/feed-generation.spec.ts @@ -0,0 +1,119 @@ +/* + * Copyright Magmodules.eu. All rights reserved. + * See COPYING.txt for license details. + */ + +import { test, expect } from '@playwright/test'; +import ChannableApi from 'Services/ChannableApi'; + +const api = new ChannableApi(); + +const FEED_TOKEN = process.env.CHANNABLE_TOKEN || 'e2e-test-token'; +const STORE_ID = 1; + +const CONFIG = { + 'magmodules_channable/general/enable': '1', +}; + +test.describe('Feed Generation', () => { + test.beforeAll(async ({}, testInfo) => { + const baseURL = testInfo.project.use.baseURL!; + await api.setMagentoConfig(baseURL, CONFIG); + }); + + test('feed endpoint returns valid JSON without errors', async ({ request }) => { + const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=${FEED_TOKEN}&page=1`); + + expect(response.status()).toBe(200); + + const body = await response.json(); + expect(body).not.toHaveProperty('error'); + }); + + test('feed endpoint returns products array', async ({ request }) => { + const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=${FEED_TOKEN}&page=1`); + + const body = await response.json(); + expect(body).toHaveProperty('products'); + expect(Array.isArray(body.products)).toBe(true); + }); + + test('feed products contain required id and title fields', async ({ request }) => { + const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=${FEED_TOKEN}&page=1`); + + const body = await response.json(); + const products = body.products ?? []; + + // Skip if no products in feed (empty catalog) + if (products.length === 0) return; + + for (const product of products) { + // Every product row must have an id and title + expect(product).toHaveProperty('id'); + expect(product).toHaveProperty('title'); + expect(product.id).toBeTruthy(); + } + }); + + test('feed does not crash with visibility filter enabled', async ({ request }, testInfo) => { + const baseURL = testInfo.project.use.baseURL!; + + // Enable visibility filter and include "Not Visible Individually" (value 1) + await api.setMagentoConfig(baseURL, { + ...CONFIG, + 'magmodules_channable/filter/visbility_enabled': '1', + 'magmodules_channable/filter/visbility': '1', + }); + + const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=${FEED_TOKEN}&page=1`); + + expect(response.status()).toBe(200); + + const body = await response.json(); + expect(body).not.toHaveProperty('error'); + + // Reset visibility filter + await api.setMagentoConfig(baseURL, { + ...CONFIG, + 'magmodules_channable/filter/visbility_enabled': '0', + }); + }); + + test('feed returns empty for invalid token', async ({ request }) => { + const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=wrong-token&page=1`); + + expect(response.status()).toBe(200); + + const body = await response.json(); + // Should return empty array, not an error page + expect(Array.isArray(body) || (typeof body === 'object' && Object.keys(body).length === 0)).toBe(true); + }); + + test('feed returns empty when module is disabled', async ({ request }, testInfo) => { + const baseURL = testInfo.project.use.baseURL!; + + await api.setMagentoConfig(baseURL, { + 'magmodules_channable/general/enable': '0', + 'magmodules_channable/general/token': FEED_TOKEN, + }); + + const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=${FEED_TOKEN}&page=1`); + + expect(response.status()).toBe(200); + + const body = await response.json(); + expect(Array.isArray(body) || (typeof body === 'object' && Object.keys(body).length === 0)).toBe(true); + + // Re-enable + await api.setMagentoConfig(baseURL, CONFIG); + }); + + test('single product feed via pid parameter', async ({ request }) => { + const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=${FEED_TOKEN}&pid=1`); + + expect(response.status()).toBe(200); + + const body = await response.json(); + expect(body).not.toHaveProperty('error'); + }); +}); diff --git a/Test/End-2-end/tests/order/order-import.spec.ts b/Test/End-2-end/tests/order/order-import.spec.ts index 1ed0b20e..54489c9f 100644 --- a/Test/End-2-end/tests/order/order-import.spec.ts +++ b/Test/End-2-end/tests/order/order-import.spec.ts @@ -13,6 +13,9 @@ const orderViewPage = new OrderViewPage(); const customerViewPage = new CustomerViewPage(); const PRODUCT_ID = parseInt(process.env.PRODUCT_ID || '1', 10); +const SECOND_STORE_CODE = 'second_store'; + +let secondStoreId: number | null = null; const CONFIG_BASE = 'magmodules_channable_marketplace/order'; @@ -210,11 +213,29 @@ const testCases = [ expect(baseCurrencyTotal).toBeGreaterThan(0); }, }, + { + title: 'Customer created in correct store view', + config: { + [`${CONFIG_BASE}/import_customer`]: '1', + }, + orderOverrides: { + email: `e2e-storeview-${Date.now()}@magmodules.eu`, + }, + secondStore: true, + skipOrderView: true, + setup: async () => { + secondStoreId = channableApi.ensureSecondStoreView(SECOND_STORE_CODE); + }, + assert: async (page, incrementId, orderData, baseURL) => { + const customer = await channableApi.getCustomerByEmail(baseURL, orderData.customer.email); + expect(customer.store_id).toBe(secondStoreId); + }, + }, ]; for (const testCase of testCases) { test(`Order import: ${testCase.title}`, async ({ page, baseURL }) => { - // 0. Run optional setup (e.g. currency rates) + // 0. Run optional setup (e.g. currency rates, store views) if (testCase.setup) { await testCase.setup(); } @@ -231,14 +252,20 @@ for (const testCase of testCases) { ...testCase.orderOverrides, }); - const response = await channableApi.postOrder(baseURL, orderData); + let storeId = 1; + if (testCase.secondStore && secondStoreId) { + storeId = secondStoreId; + } + const response = await channableApi.postOrder(baseURL, orderData, storeId); const incrementId = getOrderIncrementId(response); console.log(`Order created: ${incrementId} (${testCase.title})`); - // 3. Open order in admin - await orderViewPage.openByIncrementId(page, incrementId); + // 3. Open order in admin (skip for tests that only need API assertions) + if (!testCase.skipOrderView) { + await orderViewPage.openByIncrementId(page, incrementId); + } // 4. Run test-specific assertions - await testCase.assert(page, incrementId); + await testCase.assert(page, incrementId, orderData, baseURL); }); }