diff --git a/Service/Order/Quote/CustomerHandler.php b/Service/Order/Quote/CustomerHandler.php index 3c16627..3abb563 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')); diff --git a/Test/End-2-end/support/services/ChannableApi.ts b/Test/End-2-end/support/services/ChannableApi.ts index 29ad875..5abea16 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 0000000..1716a7f --- /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 1ed0b20..54489c9 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); }); }