From 75df927eb126aacfc0c4a422a6a475358ef6193e Mon Sep 17 00:00:00 2001 From: shashwata Date: Sun, 27 Oct 2024 22:58:34 +0600 Subject: [PATCH 01/12] Add LIve chat tests --- tests/pw/feature-map/feature-map.yml | 19 +++-- tests/pw/pages/basePage.ts | 14 +++- tests/pw/pages/liveChatPage.ts | 92 +++++++++++++++++++++++ tests/pw/pages/selectors.ts | 82 +++++++++++++------- tests/pw/pages/settingsPage.ts | 24 ++++++ tests/pw/pages/singleProductPage.ts | 2 +- tests/pw/pages/singleStorePage.ts | 2 +- tests/pw/pages/storeSupportsPage.ts | 2 +- tests/pw/pages/vendorSettingsPage.ts | 9 +++ tests/pw/tests/e2e/_env.setup.ts | 4 + tests/pw/tests/e2e/liveChat.spec.ts | 77 +++++++++++++++++++ tests/pw/tests/e2e/settings.spec.ts | 4 + tests/pw/tests/e2e/vendorSettings.spec.ts | 4 + tests/pw/types/environment.d.ts | 2 + tests/pw/utils/dbData.ts | 15 ++-- tests/pw/utils/interfaces.ts | 14 ++++ tests/pw/utils/testData.ts | 28 ++++++- 17 files changed, 343 insertions(+), 51 deletions(-) create mode 100644 tests/pw/pages/liveChatPage.ts create mode 100644 tests/pw/tests/e2e/liveChat.spec.ts diff --git a/tests/pw/feature-map/feature-map.yml b/tests/pw/feature-map/feature-map.yml index a13e1c4f39..19c7617c89 100644 --- a/tests/pw/feature-map/feature-map.yml +++ b/tests/pw/feature-map/feature-map.yml @@ -483,6 +483,7 @@ admin can set Dokan email verification settings: true admin can set Dokan shipping status settings: true admin can set Dokan quote settings: true + admin can set Dokan live chat settings: true admin can set Dokan rma settings: true admin can set Dokan wholesale settings: true admin can set Dokan eu compliance settings: true @@ -759,16 +760,18 @@ - page: 'Live Chat' features: admin: - admin can set chat provider: false - admin can enable chat button on vendor page: false - admin can enable chat button on product page: false + # admin can set Dokan live chat settings [duplicate]: true + admin can enable chat button on vendor page: true + admin can disable chat button on vendor page: true + admin can enable chat button on product page (above_tab): true + admin can enable chat button on product page (inside_tab): true + admin can disable chat button on product page: true vendor: - # vendor can set live chat settings [duplicate]: false - vendor can set inbox menu page: false - vendor can view inbox menu page: false - vendor can chat with customer: false + # vendor can set live chat settings [duplicate]: true + vendor can view inbox menu page: true + vendor can reply to customer message: true customer: - customer can chat with vendor: false + customer can send message to vendor: true - page: 'Live Search' features: diff --git a/tests/pw/pages/basePage.ts b/tests/pw/pages/basePage.ts index 79a05a2d90..36264f9009 100644 --- a/tests/pw/pages/basePage.ts +++ b/tests/pw/pages/basePage.ts @@ -961,6 +961,12 @@ export class BasePage { // await locator.pressSequentially(text); } + async clickFrameSelectorAndWaitForResponse(frame: string, subUrl: string, frameSelector: string, code = 200): Promise { + const locator = this.page.frameLocator(frame).locator(frameSelector); + const [response] = await Promise.all([this.page.waitForResponse(resp => resp.url().includes(subUrl) && resp.status() === code), locator.click()]); + return response; + } + /** * Locator methods [using playwright locator class] */ @@ -1537,7 +1543,13 @@ export class BasePage { }, options); } - // assert element to contain text + // assert frame element to be visible + async toBeVisibleFrameLocator(frame: string, frameSelector: string, options?: { timeout?: number; visible?: boolean } | undefined) { + const locator = this.page.frameLocator(frame).locator(frameSelector); + await expect(locator).toBeVisible(options); + } + + // assert frame element to contain text async toContainTextFrameLocator(frame: string, frameSelector: string, text: string | RegExp, options?: { timeout?: number; intervals?: number[] }): Promise { await this.toPass(async () => { const locator = this.page.frameLocator(frame).locator(frameSelector); diff --git a/tests/pw/pages/liveChatPage.ts b/tests/pw/pages/liveChatPage.ts new file mode 100644 index 0000000000..fffa265ea6 --- /dev/null +++ b/tests/pw/pages/liveChatPage.ts @@ -0,0 +1,92 @@ +import { Page } from '@playwright/test'; +import { BasePage } from '@pages/basePage'; +import { selector } from '@pages/selectors'; +import { data } from '@utils/testData'; +import { helpers } from '@utils/helpers'; + +// selectors +const liveChatVendor = selector.vendor.vInbox; +const liveChatCustomer = selector.customer.cLiveChat; +const singleStoreCustomer = selector.customer.cSingleStore; +const singleProductCustomer = selector.customer.cSingleProduct; + +export class LiveChatPage extends BasePage { + constructor(page: Page) { + super(page); + } + + async gotoSingleStore(storeName: string): Promise { + await this.goIfNotThere(data.subUrls.frontend.vendorDetails(helpers.slugify(storeName)), 'networkidle'); + } + + async goToProductDetails(productName: string): Promise { + await this.goIfNotThere(data.subUrls.frontend.productDetails(helpers.slugify(productName))); + } + + // vendor + + // vendor inbox render properly + async vendorInboxRenderProperly(): Promise { + await this.goIfNotThere(data.subUrls.frontend.vDashboard.inbox); + + // chat persons, chat box, chat text box, send button is visible + await this.toBeVisibleFrameLocator(liveChatVendor.liveChatIframe, liveChatVendor.chatPersons); + await this.toBeVisibleFrameLocator(liveChatVendor.liveChatIframe, liveChatVendor.chatBox); + await this.toBeVisibleFrameLocator(liveChatVendor.liveChatIframe, liveChatVendor.chatTextBox); + await this.toBeVisibleFrameLocator(liveChatVendor.liveChatIframe, liveChatVendor.sendButton); + } + + // vendor send message to vendor customer + async sendMessageToCustomer(chatPerson: string, message: string): Promise { + await this.goIfNotThere(data.subUrls.frontend.vDashboard.inbox); + await this.clickFrameSelector(liveChatVendor.liveChatIframe, liveChatVendor.chatPerson(chatPerson)); + await this.typeFrameSelector(liveChatVendor.liveChatIframe, liveChatVendor.chatTextBox, message); + await this.clickFrameSelectorAndWaitForResponse(liveChatVendor.liveChatIframe, data.subUrls.frontend.talkjs, liveChatVendor.sendButton); + await this.toBeVisibleFrameLocator(liveChatVendor.liveChatIframe, liveChatVendor.sentMessage(message)); + } + + // customer + + // customer send message to vendor customer + async sendMessageToVendor(storename: string, message: string): Promise { + await this.gotoSingleStore(storename); + await this.click(singleStoreCustomer.storeTabs.chatNow); + await this.toBeVisible(liveChatCustomer.liveChatIframe); + await this.typeFrameSelector(liveChatCustomer.liveChatIframe, liveChatCustomer.chatTextBox, message); + await this.clickFrameSelectorAndWaitForResponse(liveChatCustomer.liveChatIframe, data.subUrls.frontend.talkjs, liveChatCustomer.sendButton); + await this.toBeVisibleFrameLocator(liveChatCustomer.liveChatIframe, liveChatCustomer.sentMessage(message)); + } + + async viewLiveChatButtonOnStore(storename: string, disable = false) { + await this.gotoSingleStore(storename); + if (!disable) { + await this.toBeVisible(singleStoreCustomer.storeTabs.chatNow); + } else { + await this.notToBeVisible(singleStoreCustomer.storeTabs.chatNow); + } + } + + async viewLiveChatButtonOnProduct(productName: string, option: string) { + await this.goToProductDetails(productName); + + switch (option) { + case 'above-tab': + await this.toBeVisible(singleProductCustomer.productDetails.chatNow); + break; + + case 'inside-tab': + await this.click(singleProductCustomer.menus.vendorInfo); + await this.toBeVisible(singleProductCustomer.productDetails.chatNow); + break; + + case 'dont-show': + await this.notToBeVisible(singleProductCustomer.productDetails.chatNow); + await this.click(singleProductCustomer.menus.vendorInfo); + await this.notToBeVisible(singleProductCustomer.productDetails.chatNow); + break; + + default: + break; + } + } +} diff --git a/tests/pw/pages/selectors.ts b/tests/pw/pages/selectors.ts index 673d21eafb..06308d62a0 100644 --- a/tests/pw/pages/selectors.ts +++ b/tests/pw/pages/selectors.ts @@ -2373,25 +2373,23 @@ export const selector = { // Live Chat liveChat: { - enableLiveChat: '#dokan_live_chat\\[enable\\]', - chatProviderFacebookMessenger: '#dokan_live_chat\\[provider\\]\\[messenger\\]', - chatProviderTalkJs: '#dokan_live_chat\\[provider\\]\\[talkjs\\]', - chatProviderTawkTo: '#dokan_live_chat\\[provider\\]\\[tawkto\\]', - chatProviderWhatsApp: '#dokan_live_chat\\[provider\\]\\[whatsapp\\]', + enableLiveChat: '//label[@for="dokan_live_chat[enable]"]', + + chatProvider: (provider: string) => `//label[contains(@for,'${provider}-provider')]`, // Fb - messengerColor: '.button > span', + messengerColor: 'div.color-picker-container span.dashicons', // Talkjs - talkJsAppId: '#dokan_live_chat\\[app_id\\]', - talkJsAppSecret: '#dokan_live_chat\\[app_secret\\]', + talkJsAppId: 'input#dokan_live_chat\\[app_id\\]', + talkJsAppSecret: 'input#dokan_live_chat\\[app_secret\\]', // Whatsapp - openingPattern: '#dokan_live_chat\\[wa_opening_method\\]', - preFilledMessage: '#dokan_live_chat\\[wa_pre_filled_message\\]', + openingPattern: 'select#dokan_live_chat\\[wa_opening_method\\]', + preFilledMessage: 'textarea#dokan_live_chat\\[wa_pre_filled_message\\]', // Chat Button - chatButtonOnVendorPage: '#dokan_live_chat\\[chat_button_seller_page\\]', + chatButtonOnVendorPage: '//label[@for="dokan_live_chat[chat_button_seller_page]"]', chatButtonOnProductPage: '#dokan_live_chat\\[chat_button_product_page\\]', liveChatSaveChanges: '#submit', }, @@ -5869,7 +5867,7 @@ export const selector = { }, }, - // Settings + // settings vSettings: { store: '.store > a', addons: '.product-addon > a', @@ -5883,12 +5881,25 @@ export const selector = { storeSEO: '.seo > a', }, - // Store Settings + // inbox + vInbox: { + chatPersons: 'div#hub', + chatPerson: (personName: string) => `//div[@class="ConversationListItem__conversation-name" and normalize-space(text())="${personName}"]/../../..`, + liveChatIframe: '(//iframe[@name="____talkjs__chat__ui_internal"])[last()]', + liveChatLauncher: 'a#__talkjs_launcher', + + chatBox: 'div#chat-box', + chatTextBox: '//div[@role="textbox"]', + sendButton: 'button.send-button', + sentMessage: (message: string) => `//div[@id="chat-box"]//span[@class="EntityTreeRenderer" and normalize-space(text())="${message}"]`, + }, + + // store settings vStoreSettings: { settingsText: '.dokan-settings-content h1', visitStore: '//a[normalize-space()="Visit Store"]', - // Wp Image Upload + // wp image upload wpUploadFiles: '#menu-item-upload', uploadedMedia: '.attachment-preview', selectFiles: '//div[@class="supports-drag-drop" and @style="position: relative;"]//button[@class="browser button button-hero"]', @@ -5906,7 +5917,7 @@ export const selector = { uploadedProfilePicture: 'div#dokan-profile-picture-wrapper div.gravatar-wrap', removeProfilePictureImage: '.dokan-close.dokan-remove-gravatar-image', - // Basic Store Info + // basic store Info storeName: '#dokan_store_name', phoneNo: '#setting_phone', @@ -5923,7 +5934,7 @@ export const selector = { editLocation: '.store-pickup-location-edit-btn', locationName: '#store-location-name-input', - // Address + // address address: { street: '#dokan_address\\[street_1\\]', street2: '#dokan_address\\[street_2\\]', @@ -5936,7 +5947,7 @@ export const selector = { deleteSaveLocation: '.store-pickup-location-delete-btn', }, - // Company Info + // company info companyInfo: { companyName: '#settings_dokan_company_name', companyId: '#settings_dokan_company_id_number', @@ -5945,18 +5956,18 @@ export const selector = { bankIban: '#setting_bank_iban', }, - // Email + // email email: '//label[contains(text(), "Email")]/..//input[@type="checkbox"]', - // Map + // map map: '#dokan-map-add', - // Terms and Conditions + // terms and conditions termsAndConditions: '//label[contains(text(), "Terms and Conditions")]/..//input[@type="checkbox"]', termsAndConditionsIframe: '#dokan_tnc_text iframe', termsAndConditionsHtmlBody: '#tinymce', - // Store Opening Closing Time + // store opening closing time storeOpeningClosingTime: '#dokan-store-time-enable', // lite locators @@ -5977,7 +5988,7 @@ export const selector = { storeOpenNotice: '//input[@name="dokan_store_open_notice"]', storeCloseNotice: '//input[@name="dokan_store_close_notice"]', - // Vacation + // vacation goToVacation: '#dokan-seller-vacation-activate', closingStyle: 'label .form-control', setVacationMessageInstantly: '//textarea[@id="dokan-seller-vacation-message" and @name="setting_vacation_message"]', @@ -5999,21 +6010,24 @@ export const selector = { hideProductPrice: 'input#catalog_mode_hide_product_price', enableRequestQuoteSupport: 'input#catalog_mode_request_a_quote_support', - // Discount + // discount enableStoreWideDiscount: '#lbl_setting_minimum_quantity', minimumOrderAmount: '#setting_minimum_order_amount', percentage: '#setting_order_percentage', - // Biography + // biography biographyIframe: '#wp-vendor_biography-wrap iframe', biographyHtmlBody: '#tinymce', - // Store Support + // store support showSupportButtonInStore: '#support_checkbox', showSupportButtonInSingleProduct: '#support_checkbox_product', supportButtonText: '#dokan_support_btn_name', - // Min-Max + // live chat + liveChat: 'input#live_chat', + + // min-max minMax: { minimumAmountToPlaceAnOrder: 'input#min_amount_to_order', maximumAmountToPlaceAnOrder: 'input#max_amount_to_order', @@ -6917,6 +6931,7 @@ export const selector = { price: '//div[@class="summary entry-summary"]//p[@class="price"]', quantity: 'div.quantity input.qty', addToCart: 'button.single_add_to_cart_button', + chatNow: 'button.dokan-live-chat', viewCart: '.woocommerce .woocommerce-message > .button', category: '.product_meta .posted_in', @@ -7265,9 +7280,10 @@ export const selector = { // Store Tabs storeTabs: { - follow: '.dokan-follow-store-button', + follow: 'button.dokan-follow-store-button', getSupport: 'button.dokan-store-support-btn', - share: '.dokan-share-btn', + chatNow: 'button.dokan-live-chat', + share: 'button.dokan-share-btn', products: '//div[@class="dokan-store-tabs"]//a[contains(text(),"Products")]', toc: '//div[@class="dokan-store-tabs"]//a[contains(text(),"Terms and Conditions")]', @@ -7748,6 +7764,16 @@ export const selector = { }, }, + cLiveChat: { + liveChatIframe: '(//div[ contains(@id, "__talkjs_popup_container") and not (@style="display: none;") ]//iframe[@name="____talkjs__chat__ui_internal"])[last()]', + // liveChatIframe: '(//iframe[@name="____talkjs__chat__ui_internal"])[last()]', + liveChatLauncher: 'a#__talkjs_launcher', + chatBox: 'div#chat-box', + chatTextBox: '//div[@role="textbox"]', + sendButton: 'button.send-button', + sentMessage: (message: string) => `//span[@class="EntityTreeRenderer" and normalize-space(text())="${message}"]`, + }, + cOrderReceived: { orderReceivedHeading: '//h1[normalize-space()="Order received"]', orderReceivedSuccessMessage: '.woocommerce-notice.woocommerce-notice--success.woocommerce-thankyou-order-received', diff --git a/tests/pw/pages/settingsPage.ts b/tests/pw/pages/settingsPage.ts index f5977f1a28..199d6113ee 100644 --- a/tests/pw/pages/settingsPage.ts +++ b/tests/pw/pages/settingsPage.ts @@ -414,6 +414,30 @@ export class SettingsPage extends AdminPage { await this.toContainText(settingsAdmin.dokanUpdateSuccessMessage, quote.saveSuccessMessage); } + // Admin Set Dokan live chat Settings + async setDokanLiveChatSettings(liveChat: dokanSettings['liveChat']) { + await this.goToDokanSettings(); + await this.click(settingsAdmin.menus.liveChat); + + // liveChat Settings + await this.enableSwitcher(settingsAdmin.liveChat.enableLiveChat); + await this.click(settingsAdmin.liveChat.chatProvider(liveChat.chatProvider)); + await this.clearAndType(settingsAdmin.liveChat.talkJsAppId, liveChat.talkJsAppId); + await this.clearAndType(settingsAdmin.liveChat.talkJsAppSecret, liveChat.talkJsAppSecret); + await this.enableSwitcher(settingsAdmin.liveChat.chatButtonOnVendorPage); + await this.selectByValue(settingsAdmin.liveChat.chatButtonOnProductPage, liveChat.chatButtonPosition); + + // save settings + await this.clickAndWaitForResponseAndLoadState(data.subUrls.ajax, settingsAdmin.liveChat.liveChatSaveChanges); + + await this.toHaveBackgroundColor(settingsAdmin.liveChat.enableLiveChat + '//span', 'rgb(0, 144, 255)'); + await this.toHaveClass(settingsAdmin.liveChat.chatProvider(liveChat.chatProvider), 'checked'); + await this.toHaveValue(settingsAdmin.liveChat.talkJsAppId, liveChat.talkJsAppId); + await this.toHaveValue(settingsAdmin.liveChat.talkJsAppSecret, liveChat.talkJsAppSecret); + await this.toHaveBackgroundColor(settingsAdmin.liveChat.chatButtonOnVendorPage + '//span', 'rgb(0, 144, 255)'); + await this.toHaveSelectedValue(settingsAdmin.liveChat.chatButtonOnProductPage, liveChat.chatButtonPosition); + } + // Admin Set Dokan Rma Settings async setDokanRmaSettings(rma: dokanSettings['rma']) { await this.goToDokanSettings(); diff --git a/tests/pw/pages/singleProductPage.ts b/tests/pw/pages/singleProductPage.ts index 029634ebae..951147d02b 100644 --- a/tests/pw/pages/singleProductPage.ts +++ b/tests/pw/pages/singleProductPage.ts @@ -21,7 +21,7 @@ export class SingleProductPage extends CustomerPage { await this.goToProductDetails(productName); // basic details are visible - const { viewCart, euComplianceData, productAddedSuccessMessage, productWithQuantityAddedSuccessMessage, ...productDetails } = singleProductCustomer.productDetails; + const { viewCart, chatNow, euComplianceData, productAddedSuccessMessage, productWithQuantityAddedSuccessMessage, ...productDetails } = singleProductCustomer.productDetails; await this.multipleElementVisible(productDetails); // description elements are visible diff --git a/tests/pw/pages/singleStorePage.ts b/tests/pw/pages/singleStorePage.ts index 4f280c7f77..d7c6efb460 100644 --- a/tests/pw/pages/singleStorePage.ts +++ b/tests/pw/pages/singleStorePage.ts @@ -30,7 +30,7 @@ export class SingleStorePage extends CustomerPage { await this.toBeVisible(singleStoreCustomer.storeTabs.products); // await this.toBeVisible(singleStoreCustomer.storeTabs.toc); // todo: need vendor toc } else { - const { toc, ...storeTabs } = singleStoreCustomer.storeTabs; + const { toc, chatNow, ...storeTabs } = singleStoreCustomer.storeTabs; await this.multipleElementVisible(storeTabs); // eu compliance data is visible diff --git a/tests/pw/pages/storeSupportsPage.ts b/tests/pw/pages/storeSupportsPage.ts index ef1a546aa5..a85c3bf632 100644 --- a/tests/pw/pages/storeSupportsPage.ts +++ b/tests/pw/pages/storeSupportsPage.ts @@ -424,7 +424,7 @@ export class StoreSupportsPage extends AdminPage { await this.toBeVisible(supportsTicketsCustomer.supportTicketDetails.orderReference.orderReferenceLink(orderId)); } - // customer send message to support ticket + // customer send message to support ticket async sendMessageToSupportTicket(supportTicketId: string, supportTicket: customer['supportTicket']): Promise { const message = supportTicket.message(); await this.goIfNotThere(data.subUrls.frontend.supportTicketDetails(supportTicketId)); diff --git a/tests/pw/pages/vendorSettingsPage.ts b/tests/pw/pages/vendorSettingsPage.ts index 69cda561f5..a63feca8e8 100644 --- a/tests/pw/pages/vendorSettingsPage.ts +++ b/tests/pw/pages/vendorSettingsPage.ts @@ -236,6 +236,10 @@ export class VendorSettingsPage extends VendorPage { await this.storeSupportSettings(vendorInfo.supportButtonText); break; + case 'liveChat': + await this.liveChatSettings(vendorInfo.liveChat); + break; + case 'min-max': await this.minMaxSettings(vendorInfo.minMax); break; @@ -423,6 +427,11 @@ export class VendorSettingsPage extends VendorPage { } } + // vendor set liveChat settings + async liveChatSettings(liveChat: vendor['vendorInfo']['liveChat']): Promise { + await this.check(settingsVendor.liveChat); + } + // vendor set minmax settings async minMaxSettings(minMax: vendor['vendorInfo']['minMax']): Promise { await this.clearAndType(settingsVendor.minMax.minimumAmountToPlaceAnOrder, minMax.minimumAmount); diff --git a/tests/pw/tests/e2e/_env.setup.ts b/tests/pw/tests/e2e/_env.setup.ts index 7d9e28b771..2813f2706e 100644 --- a/tests/pw/tests/e2e/_env.setup.ts +++ b/tests/pw/tests/e2e/_env.setup.ts @@ -229,6 +229,10 @@ setup.describe('setup dokan settings', () => { await dbUtils.setOptionValue(dbData.dokan.optionName.quote, dbData.dokan.quoteSettings); }); + setup('admin set dokan live chat settings', { tag: ['@pro'] }, async () => { + await dbUtils.setOptionValue(dbData.dokan.optionName.liveChat, dbData.dokan.liveChatSettings); + }); + setup('admin set dokan rma settings', { tag: ['@pro'] }, async () => { await dbUtils.setOptionValue(dbData.dokan.optionName.rma, dbData.dokan.rmaSettings); }); diff --git a/tests/pw/tests/e2e/liveChat.spec.ts b/tests/pw/tests/e2e/liveChat.spec.ts new file mode 100644 index 0000000000..37d988ee4d --- /dev/null +++ b/tests/pw/tests/e2e/liveChat.spec.ts @@ -0,0 +1,77 @@ +import { test, Page } from '@playwright/test'; +import { LiveChatPage } from '@pages/liveChatPage'; +import { dbUtils } from '@utils/dbUtils'; +import { data } from '@utils/testData'; +import { dbData } from '@utils/dbData'; + +test.describe('Live chat test', () => { + let vendor: LiveChatPage; + let customer: LiveChatPage; + let vPage: Page, cPage: Page; + + test.beforeAll(async ({ browser }) => { + const vendorContext = await browser.newContext(data.auth.vendorAuth); + vPage = await vendorContext.newPage(); + vendor = new LiveChatPage(vPage); + + const customerContext = await browser.newContext(data.auth.customerAuth); + cPage = await customerContext.newPage(); + customer = new LiveChatPage(cPage); + + // todo: enable vendor live chat + }); + + test.afterAll(async () => { + await dbUtils.setOptionValue(dbData.dokan.optionName.liveChat, dbData.dokan.liveChatSettings); + await cPage.close(); + }); + + // admin + + test('admin can enable chat button on vendor page', { tag: ['@pro', '@admin'] }, async () => { + await dbUtils.updateOptionValue(dbData.dokan.optionName.liveChat, { chat_button_product_page: 'on' }); + await customer.viewLiveChatButtonOnStore(data.predefined.vendorInfo.shopName); + }); + + test('admin can disable chat button on vendor page', { tag: ['@pro', '@admin'] }, async () => { + test.skip(true, 'Has Dokan Issues'); + await dbUtils.updateOptionValue(dbData.dokan.optionName.liveChat, { chat_button_product_page: 'off' }); + await customer.viewLiveChatButtonOnStore(data.predefined.vendorInfo.shopName, true); + // reset + await dbUtils.updateOptionValue(dbData.dokan.optionName.liveChat, { chat_button_product_page: 'on' }); + }); + + test('admin can enable chat button on product page (above_tab)', { tag: ['@pro', '@admin'] }, async () => { + await dbUtils.updateOptionValue(dbData.dokan.optionName.liveChat, { chat_button_product_page: 'above_tab' }); + await customer.viewLiveChatButtonOnProduct(data.predefined.simpleProduct.product1.name, 'above-tab'); + }); + + test('admin can enable chat button on product page (inside_tab)', { tag: ['@pro', '@admin'] }, async () => { + test.skip(true, 'Has Dokan Issues'); + await dbUtils.updateOptionValue(dbData.dokan.optionName.liveChat, { chat_button_product_page: 'inside_tab' }); + await customer.viewLiveChatButtonOnProduct(data.predefined.simpleProduct.product1.name, 'inside-tab'); + }); + + test('admin can disable chat button on product page', { tag: ['@pro', '@admin'] }, async () => { + test.skip(true, 'Has Dokan Issues'); + await dbUtils.updateOptionValue(dbData.dokan.optionName.liveChat, { chat_button_product_page: 'dont_show' }); + await customer.viewLiveChatButtonOnProduct(data.predefined.simpleProduct.product1.name, 'dont-show'); + }); + + // vendor + + test('vendor can view inbox menu page', { tag: ['@pro', '@exploratory', '@vendor'] }, async () => { + await vendor.vendorInboxRenderProperly(); + }); + + test('vendor can reply to customer message', { tag: ['@pro', '@customer'] }, async () => { + await customer.sendMessageToVendor('vendor1store', data.uniqueId.nanoIdRandom()); + await vendor.sendMessageToCustomer(data.predefined.customerInfo.username1, data.uniqueId.nanoIdRandom()); + }); + + // customer + + test('customer can send message to vendor', { tag: ['@pro', '@customer'] }, async () => { + await customer.sendMessageToVendor(data.predefined.vendorInfo.shopName, data.uniqueId.nanoIdRandom()); + }); +}); diff --git a/tests/pw/tests/e2e/settings.spec.ts b/tests/pw/tests/e2e/settings.spec.ts index d45586758b..7edaf5096c 100644 --- a/tests/pw/tests/e2e/settings.spec.ts +++ b/tests/pw/tests/e2e/settings.spec.ts @@ -95,6 +95,10 @@ test.describe('Settings test', () => { await admin.setDokanQuoteSettings(data.dokanSettings.quote); }); + test('admin can set Dokan live chat settings', { tag: ['@pro', '@admin'] }, async () => { + await admin.setDokanLiveChatSettings(data.dokanSettings.liveChat); + }); + test('admin can set Dokan rma settings', { tag: ['@pro', '@admin'] }, async () => { await admin.setDokanRmaSettings(data.dokanSettings.rma); }); diff --git a/tests/pw/tests/e2e/vendorSettings.spec.ts b/tests/pw/tests/e2e/vendorSettings.spec.ts index 57c3fc151c..611624476a 100644 --- a/tests/pw/tests/e2e/vendorSettings.spec.ts +++ b/tests/pw/tests/e2e/vendorSettings.spec.ts @@ -106,6 +106,10 @@ test.describe('Vendor settings test', () => { await vendor.setStoreSettings(data.vendor.vendorInfo, 'store-support'); }); + test('vendor can set live chat settings', { tag: ['@pro', '@vendor'] }, async () => { + await vendor.setStoreSettings(data.vendor.vendorInfo, 'liveChat'); + }); + test('vendor can set min-max settings', { tag: ['@pro', '@vendor'] }, async () => { await vendor.setStoreSettings(data.vendor.vendorInfo, 'min-max'); // disable min-max diff --git a/tests/pw/types/environment.d.ts b/tests/pw/types/environment.d.ts index 0774a54883..a3915c282d 100644 --- a/tests/pw/types/environment.d.ts +++ b/tests/pw/types/environment.d.ts @@ -26,6 +26,8 @@ declare global { GMAP: string; MAPBOX: string; LICENSE_KEY: string; + TALKJS_APP_ID: string; + TALKJS_APP_SECRET: string; DOKAN_PRO: boolean; SITE_LANGUAGE: string; SITE_TITLE: string; diff --git a/tests/pw/utils/dbData.ts b/tests/pw/utils/dbData.ts index f3cea8a3bd..612b11e22e 100644 --- a/tests/pw/utils/dbData.ts +++ b/tests/pw/utils/dbData.ts @@ -1,4 +1,4 @@ -const { BASE_URL, GMAP, MAPBOX, LICENSE_KEY } = process.env; +const { BASE_URL, GMAP, MAPBOX, LICENSE_KEY, TALKJS_APP_ID, TALKJS_APP_SECRET } = process.env; export const dbData = { dokan: { @@ -20,7 +20,7 @@ export const dbData = { // socialApi: 'dokan_social_api', shippingStatus: 'dokan_shipping_status_setting', quote: 'dokan_quote_settings', - // liveChat: 'dokan_live_chat', + liveChat: 'dokan_live_chat', rma: 'dokan_rma', wholesale: 'dokan_wholesale', euCompliance: 'dokan_germanized', @@ -897,15 +897,16 @@ export const dbData = { }, liveChatSettings: { - enable: 'off', - provider: 'messenger', + enable: 'on', + provider: 'talkjs', theme_color: '#0084FF', - app_id: '', - app_secret: '', + app_id: TALKJS_APP_ID, + app_secret: TALKJS_APP_SECRET, wa_opening_method: 'in_app', wa_pre_filled_message: 'Hello {store_name}, I have an enquiry regarding your store at {store_url}', chat_button_seller_page: 'on', - chat_button_product_page: 'above_tab', + chat_button_product_page: 'above_tab', // above_tab, inside_tab, dont_show + dashboard_menu_manager: [], }, rmaSettings: { diff --git a/tests/pw/utils/interfaces.ts b/tests/pw/utils/interfaces.ts index de232d85d3..2ee333ca59 100644 --- a/tests/pw/utils/interfaces.ts +++ b/tests/pw/utils/interfaces.ts @@ -815,6 +815,10 @@ export interface vendor { discountPercentage: string; }; + liveChat: { + pageId: string; + }; + minMax: { minimumProductQuantity: string; maximumProductQuantity: string; @@ -1692,6 +1696,16 @@ export interface dokanSettings { saveSuccessMessage: string; }; + // Rma Settings + liveChat: { + settingTitle: string; + chatProvider: string; + talkJsAppId: string; + talkJsAppSecret: string; + chatButtonPosition: string; + saveSuccessMessage: string; + }; + // Rma Settings rma: { settingTitle: string; diff --git a/tests/pw/utils/testData.ts b/tests/pw/utils/testData.ts index 0eca4b12d8..6c852c6dfc 100644 --- a/tests/pw/utils/testData.ts +++ b/tests/pw/utils/testData.ts @@ -23,6 +23,8 @@ const { GMAP, MAPBOX, LICENSE_KEY, + TALKJS_APP_ID, + TALKJS_APP_SECRET, } = process.env; const basicAuth = (username: string, password: string) => 'Basic ' + Buffer.from(username + ':' + password).toString('base64'); @@ -1004,10 +1006,10 @@ export const data = { storeListingSort: 'store-listing/?stores_orderby', cart: 'cart', checkout: 'checkout', - addToCart: '?wc-ajax=add_to_cart', - applyCoupon: '?wc-ajax=apply_coupon', - removeCoupon: '?wc-ajax=remove_coupon', - refreshedFragment: '?wc-ajax=get_refreshed_fragments', + addToCart: 'wc-ajax=add_to_cart', + applyCoupon: 'wc-ajax=apply_coupon', + removeCoupon: 'wc-ajax=remove_coupon', + refreshedFragment: 'wc-ajax=get_refreshed_fragments', placeOrder: '?wc-ajax=checkout', billingAddress: 'my-account/edit-address/billing', shippingAddress: 'my-account/edit-address/shipping', @@ -1024,6 +1026,7 @@ export const data = { quoteDetails: (quotId: string) => `my-account/request-a-quote/${quotId}`, supportTicketDetails: (ticketId: string) => `my-account/support-tickets/${ticketId}`, productSubscriptionDetails: (subscriptionId: string) => `my-account/view-subscription/${subscriptionId}`, + talkjs: 'app.talkjs.com/api', productReview: 'wp-comments-post.php', submitSupport: 'wp-comments-post.php', @@ -1067,6 +1070,7 @@ export const data = { csvExport: 'dashboard/tools/csv-export', auction: 'dashboard/auction', auctionActivity: 'dashboard/auction-activity', + inbox: 'dashboard/inbox', storeSupport: 'dashboard/support', // sub menus @@ -1074,6 +1078,7 @@ export const data = { settingsAddon: 'dashboard/settings/product-addon', settingsAddonEdit: (addonId: string) => `dashboard/settings/product-addon/?edit=${addonId}`, settingsPayment: 'dashboard/settings/payment', + // payment settings paypal: 'dashboard/settings/payment-manage-paypal', bankTransfer: 'dashboard/settings/payment-manage-bank', @@ -1289,6 +1294,10 @@ export const data = { discountPercentage: '10', }, + liveChat: { + pageId: '', + }, + minMax: { minimumProductQuantity: '1', maximumProductQuantity: '20', @@ -2306,6 +2315,16 @@ export const data = { saveSuccessMessage: 'Setting has been saved successfully.', }, + // live chat + liveChat: { + settingTitle: 'Live Chat Settings', + chatProvider: 'talkjs', // messenger, talkjs, tawkto, whatsapp + talkJsAppId: TALKJS_APP_ID, + talkJsAppSecret: TALKJS_APP_SECRET, + chatButtonPosition: 'above_tab', // above_tab, inside_tab, dont_show + saveSuccessMessage: 'Setting has been saved successfully.', + }, + // Rma Settings rma: { settingTitle: 'RMA Settings', @@ -2450,6 +2469,7 @@ export const data = { uniqueId: { uuid: faker.string.uuid(), nanoId: faker.string.nanoid(10), + nanoIdRandom: () => faker.string.nanoid(10), }, // predefined test data From 536da1a2359521c5ea0cbd827a9962fab60a4b74 Mon Sep 17 00:00:00 2001 From: shashwata Date: Sun, 27 Oct 2024 23:12:11 +0600 Subject: [PATCH 02/12] Add vendor live chate settings --- tests/pw/tests/e2e/liveChat.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/pw/tests/e2e/liveChat.spec.ts b/tests/pw/tests/e2e/liveChat.spec.ts index 37d988ee4d..6ae2388efd 100644 --- a/tests/pw/tests/e2e/liveChat.spec.ts +++ b/tests/pw/tests/e2e/liveChat.spec.ts @@ -4,6 +4,8 @@ import { dbUtils } from '@utils/dbUtils'; import { data } from '@utils/testData'; import { dbData } from '@utils/dbData'; +const { VENDOR_ID } = process.env; + test.describe('Live chat test', () => { let vendor: LiveChatPage; let customer: LiveChatPage; @@ -18,7 +20,7 @@ test.describe('Live chat test', () => { cPage = await customerContext.newPage(); customer = new LiveChatPage(cPage); - // todo: enable vendor live chat + await dbUtils.updateUserMeta(VENDOR_ID, 'dokan_profile_settings', { live_chat: 'yes' }); }); test.afterAll(async () => { From c3816f99a1b82c9020e0b6b26730b75267137dd2 Mon Sep 17 00:00:00 2001 From: shashwata Date: Mon, 28 Oct 2024 09:39:20 +0600 Subject: [PATCH 03/12] Revert "Add vendor live chate settings" This reverts commit 536da1a2359521c5ea0cbd827a9962fab60a4b74. --- tests/pw/tests/e2e/liveChat.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/pw/tests/e2e/liveChat.spec.ts b/tests/pw/tests/e2e/liveChat.spec.ts index 6ae2388efd..37d988ee4d 100644 --- a/tests/pw/tests/e2e/liveChat.spec.ts +++ b/tests/pw/tests/e2e/liveChat.spec.ts @@ -4,8 +4,6 @@ import { dbUtils } from '@utils/dbUtils'; import { data } from '@utils/testData'; import { dbData } from '@utils/dbData'; -const { VENDOR_ID } = process.env; - test.describe('Live chat test', () => { let vendor: LiveChatPage; let customer: LiveChatPage; @@ -20,7 +18,7 @@ test.describe('Live chat test', () => { cPage = await customerContext.newPage(); customer = new LiveChatPage(cPage); - await dbUtils.updateUserMeta(VENDOR_ID, 'dokan_profile_settings', { live_chat: 'yes' }); + // todo: enable vendor live chat }); test.afterAll(async () => { From a56dc199de5cfcd21353a0ec428ba34dca0560b0 Mon Sep 17 00:00:00 2001 From: shashwata Date: Mon, 28 Oct 2024 09:40:14 +0600 Subject: [PATCH 04/12] Revert "Add LIve chat tests" This reverts commit 75df927eb126aacfc0c4a422a6a475358ef6193e. --- tests/pw/feature-map/feature-map.yml | 19 ++--- tests/pw/pages/basePage.ts | 14 +--- tests/pw/pages/liveChatPage.ts | 92 ----------------------- tests/pw/pages/selectors.ts | 82 +++++++------------- tests/pw/pages/settingsPage.ts | 24 ------ tests/pw/pages/singleProductPage.ts | 2 +- tests/pw/pages/singleStorePage.ts | 2 +- tests/pw/pages/storeSupportsPage.ts | 2 +- tests/pw/pages/vendorSettingsPage.ts | 9 --- tests/pw/tests/e2e/_env.setup.ts | 4 - tests/pw/tests/e2e/liveChat.spec.ts | 77 ------------------- tests/pw/tests/e2e/settings.spec.ts | 4 - tests/pw/tests/e2e/vendorSettings.spec.ts | 4 - tests/pw/types/environment.d.ts | 2 - tests/pw/utils/dbData.ts | 15 ++-- tests/pw/utils/interfaces.ts | 14 ---- tests/pw/utils/testData.ts | 28 +------ 17 files changed, 51 insertions(+), 343 deletions(-) delete mode 100644 tests/pw/pages/liveChatPage.ts delete mode 100644 tests/pw/tests/e2e/liveChat.spec.ts diff --git a/tests/pw/feature-map/feature-map.yml b/tests/pw/feature-map/feature-map.yml index 19c7617c89..a13e1c4f39 100644 --- a/tests/pw/feature-map/feature-map.yml +++ b/tests/pw/feature-map/feature-map.yml @@ -483,7 +483,6 @@ admin can set Dokan email verification settings: true admin can set Dokan shipping status settings: true admin can set Dokan quote settings: true - admin can set Dokan live chat settings: true admin can set Dokan rma settings: true admin can set Dokan wholesale settings: true admin can set Dokan eu compliance settings: true @@ -760,18 +759,16 @@ - page: 'Live Chat' features: admin: - # admin can set Dokan live chat settings [duplicate]: true - admin can enable chat button on vendor page: true - admin can disable chat button on vendor page: true - admin can enable chat button on product page (above_tab): true - admin can enable chat button on product page (inside_tab): true - admin can disable chat button on product page: true + admin can set chat provider: false + admin can enable chat button on vendor page: false + admin can enable chat button on product page: false vendor: - # vendor can set live chat settings [duplicate]: true - vendor can view inbox menu page: true - vendor can reply to customer message: true + # vendor can set live chat settings [duplicate]: false + vendor can set inbox menu page: false + vendor can view inbox menu page: false + vendor can chat with customer: false customer: - customer can send message to vendor: true + customer can chat with vendor: false - page: 'Live Search' features: diff --git a/tests/pw/pages/basePage.ts b/tests/pw/pages/basePage.ts index 36264f9009..79a05a2d90 100644 --- a/tests/pw/pages/basePage.ts +++ b/tests/pw/pages/basePage.ts @@ -961,12 +961,6 @@ export class BasePage { // await locator.pressSequentially(text); } - async clickFrameSelectorAndWaitForResponse(frame: string, subUrl: string, frameSelector: string, code = 200): Promise { - const locator = this.page.frameLocator(frame).locator(frameSelector); - const [response] = await Promise.all([this.page.waitForResponse(resp => resp.url().includes(subUrl) && resp.status() === code), locator.click()]); - return response; - } - /** * Locator methods [using playwright locator class] */ @@ -1543,13 +1537,7 @@ export class BasePage { }, options); } - // assert frame element to be visible - async toBeVisibleFrameLocator(frame: string, frameSelector: string, options?: { timeout?: number; visible?: boolean } | undefined) { - const locator = this.page.frameLocator(frame).locator(frameSelector); - await expect(locator).toBeVisible(options); - } - - // assert frame element to contain text + // assert element to contain text async toContainTextFrameLocator(frame: string, frameSelector: string, text: string | RegExp, options?: { timeout?: number; intervals?: number[] }): Promise { await this.toPass(async () => { const locator = this.page.frameLocator(frame).locator(frameSelector); diff --git a/tests/pw/pages/liveChatPage.ts b/tests/pw/pages/liveChatPage.ts deleted file mode 100644 index fffa265ea6..0000000000 --- a/tests/pw/pages/liveChatPage.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Page } from '@playwright/test'; -import { BasePage } from '@pages/basePage'; -import { selector } from '@pages/selectors'; -import { data } from '@utils/testData'; -import { helpers } from '@utils/helpers'; - -// selectors -const liveChatVendor = selector.vendor.vInbox; -const liveChatCustomer = selector.customer.cLiveChat; -const singleStoreCustomer = selector.customer.cSingleStore; -const singleProductCustomer = selector.customer.cSingleProduct; - -export class LiveChatPage extends BasePage { - constructor(page: Page) { - super(page); - } - - async gotoSingleStore(storeName: string): Promise { - await this.goIfNotThere(data.subUrls.frontend.vendorDetails(helpers.slugify(storeName)), 'networkidle'); - } - - async goToProductDetails(productName: string): Promise { - await this.goIfNotThere(data.subUrls.frontend.productDetails(helpers.slugify(productName))); - } - - // vendor - - // vendor inbox render properly - async vendorInboxRenderProperly(): Promise { - await this.goIfNotThere(data.subUrls.frontend.vDashboard.inbox); - - // chat persons, chat box, chat text box, send button is visible - await this.toBeVisibleFrameLocator(liveChatVendor.liveChatIframe, liveChatVendor.chatPersons); - await this.toBeVisibleFrameLocator(liveChatVendor.liveChatIframe, liveChatVendor.chatBox); - await this.toBeVisibleFrameLocator(liveChatVendor.liveChatIframe, liveChatVendor.chatTextBox); - await this.toBeVisibleFrameLocator(liveChatVendor.liveChatIframe, liveChatVendor.sendButton); - } - - // vendor send message to vendor customer - async sendMessageToCustomer(chatPerson: string, message: string): Promise { - await this.goIfNotThere(data.subUrls.frontend.vDashboard.inbox); - await this.clickFrameSelector(liveChatVendor.liveChatIframe, liveChatVendor.chatPerson(chatPerson)); - await this.typeFrameSelector(liveChatVendor.liveChatIframe, liveChatVendor.chatTextBox, message); - await this.clickFrameSelectorAndWaitForResponse(liveChatVendor.liveChatIframe, data.subUrls.frontend.talkjs, liveChatVendor.sendButton); - await this.toBeVisibleFrameLocator(liveChatVendor.liveChatIframe, liveChatVendor.sentMessage(message)); - } - - // customer - - // customer send message to vendor customer - async sendMessageToVendor(storename: string, message: string): Promise { - await this.gotoSingleStore(storename); - await this.click(singleStoreCustomer.storeTabs.chatNow); - await this.toBeVisible(liveChatCustomer.liveChatIframe); - await this.typeFrameSelector(liveChatCustomer.liveChatIframe, liveChatCustomer.chatTextBox, message); - await this.clickFrameSelectorAndWaitForResponse(liveChatCustomer.liveChatIframe, data.subUrls.frontend.talkjs, liveChatCustomer.sendButton); - await this.toBeVisibleFrameLocator(liveChatCustomer.liveChatIframe, liveChatCustomer.sentMessage(message)); - } - - async viewLiveChatButtonOnStore(storename: string, disable = false) { - await this.gotoSingleStore(storename); - if (!disable) { - await this.toBeVisible(singleStoreCustomer.storeTabs.chatNow); - } else { - await this.notToBeVisible(singleStoreCustomer.storeTabs.chatNow); - } - } - - async viewLiveChatButtonOnProduct(productName: string, option: string) { - await this.goToProductDetails(productName); - - switch (option) { - case 'above-tab': - await this.toBeVisible(singleProductCustomer.productDetails.chatNow); - break; - - case 'inside-tab': - await this.click(singleProductCustomer.menus.vendorInfo); - await this.toBeVisible(singleProductCustomer.productDetails.chatNow); - break; - - case 'dont-show': - await this.notToBeVisible(singleProductCustomer.productDetails.chatNow); - await this.click(singleProductCustomer.menus.vendorInfo); - await this.notToBeVisible(singleProductCustomer.productDetails.chatNow); - break; - - default: - break; - } - } -} diff --git a/tests/pw/pages/selectors.ts b/tests/pw/pages/selectors.ts index 06308d62a0..673d21eafb 100644 --- a/tests/pw/pages/selectors.ts +++ b/tests/pw/pages/selectors.ts @@ -2373,23 +2373,25 @@ export const selector = { // Live Chat liveChat: { - enableLiveChat: '//label[@for="dokan_live_chat[enable]"]', - - chatProvider: (provider: string) => `//label[contains(@for,'${provider}-provider')]`, + enableLiveChat: '#dokan_live_chat\\[enable\\]', + chatProviderFacebookMessenger: '#dokan_live_chat\\[provider\\]\\[messenger\\]', + chatProviderTalkJs: '#dokan_live_chat\\[provider\\]\\[talkjs\\]', + chatProviderTawkTo: '#dokan_live_chat\\[provider\\]\\[tawkto\\]', + chatProviderWhatsApp: '#dokan_live_chat\\[provider\\]\\[whatsapp\\]', // Fb - messengerColor: 'div.color-picker-container span.dashicons', + messengerColor: '.button > span', // Talkjs - talkJsAppId: 'input#dokan_live_chat\\[app_id\\]', - talkJsAppSecret: 'input#dokan_live_chat\\[app_secret\\]', + talkJsAppId: '#dokan_live_chat\\[app_id\\]', + talkJsAppSecret: '#dokan_live_chat\\[app_secret\\]', // Whatsapp - openingPattern: 'select#dokan_live_chat\\[wa_opening_method\\]', - preFilledMessage: 'textarea#dokan_live_chat\\[wa_pre_filled_message\\]', + openingPattern: '#dokan_live_chat\\[wa_opening_method\\]', + preFilledMessage: '#dokan_live_chat\\[wa_pre_filled_message\\]', // Chat Button - chatButtonOnVendorPage: '//label[@for="dokan_live_chat[chat_button_seller_page]"]', + chatButtonOnVendorPage: '#dokan_live_chat\\[chat_button_seller_page\\]', chatButtonOnProductPage: '#dokan_live_chat\\[chat_button_product_page\\]', liveChatSaveChanges: '#submit', }, @@ -5867,7 +5869,7 @@ export const selector = { }, }, - // settings + // Settings vSettings: { store: '.store > a', addons: '.product-addon > a', @@ -5881,25 +5883,12 @@ export const selector = { storeSEO: '.seo > a', }, - // inbox - vInbox: { - chatPersons: 'div#hub', - chatPerson: (personName: string) => `//div[@class="ConversationListItem__conversation-name" and normalize-space(text())="${personName}"]/../../..`, - liveChatIframe: '(//iframe[@name="____talkjs__chat__ui_internal"])[last()]', - liveChatLauncher: 'a#__talkjs_launcher', - - chatBox: 'div#chat-box', - chatTextBox: '//div[@role="textbox"]', - sendButton: 'button.send-button', - sentMessage: (message: string) => `//div[@id="chat-box"]//span[@class="EntityTreeRenderer" and normalize-space(text())="${message}"]`, - }, - - // store settings + // Store Settings vStoreSettings: { settingsText: '.dokan-settings-content h1', visitStore: '//a[normalize-space()="Visit Store"]', - // wp image upload + // Wp Image Upload wpUploadFiles: '#menu-item-upload', uploadedMedia: '.attachment-preview', selectFiles: '//div[@class="supports-drag-drop" and @style="position: relative;"]//button[@class="browser button button-hero"]', @@ -5917,7 +5906,7 @@ export const selector = { uploadedProfilePicture: 'div#dokan-profile-picture-wrapper div.gravatar-wrap', removeProfilePictureImage: '.dokan-close.dokan-remove-gravatar-image', - // basic store Info + // Basic Store Info storeName: '#dokan_store_name', phoneNo: '#setting_phone', @@ -5934,7 +5923,7 @@ export const selector = { editLocation: '.store-pickup-location-edit-btn', locationName: '#store-location-name-input', - // address + // Address address: { street: '#dokan_address\\[street_1\\]', street2: '#dokan_address\\[street_2\\]', @@ -5947,7 +5936,7 @@ export const selector = { deleteSaveLocation: '.store-pickup-location-delete-btn', }, - // company info + // Company Info companyInfo: { companyName: '#settings_dokan_company_name', companyId: '#settings_dokan_company_id_number', @@ -5956,18 +5945,18 @@ export const selector = { bankIban: '#setting_bank_iban', }, - // email + // Email email: '//label[contains(text(), "Email")]/..//input[@type="checkbox"]', - // map + // Map map: '#dokan-map-add', - // terms and conditions + // Terms and Conditions termsAndConditions: '//label[contains(text(), "Terms and Conditions")]/..//input[@type="checkbox"]', termsAndConditionsIframe: '#dokan_tnc_text iframe', termsAndConditionsHtmlBody: '#tinymce', - // store opening closing time + // Store Opening Closing Time storeOpeningClosingTime: '#dokan-store-time-enable', // lite locators @@ -5988,7 +5977,7 @@ export const selector = { storeOpenNotice: '//input[@name="dokan_store_open_notice"]', storeCloseNotice: '//input[@name="dokan_store_close_notice"]', - // vacation + // Vacation goToVacation: '#dokan-seller-vacation-activate', closingStyle: 'label .form-control', setVacationMessageInstantly: '//textarea[@id="dokan-seller-vacation-message" and @name="setting_vacation_message"]', @@ -6010,24 +5999,21 @@ export const selector = { hideProductPrice: 'input#catalog_mode_hide_product_price', enableRequestQuoteSupport: 'input#catalog_mode_request_a_quote_support', - // discount + // Discount enableStoreWideDiscount: '#lbl_setting_minimum_quantity', minimumOrderAmount: '#setting_minimum_order_amount', percentage: '#setting_order_percentage', - // biography + // Biography biographyIframe: '#wp-vendor_biography-wrap iframe', biographyHtmlBody: '#tinymce', - // store support + // Store Support showSupportButtonInStore: '#support_checkbox', showSupportButtonInSingleProduct: '#support_checkbox_product', supportButtonText: '#dokan_support_btn_name', - // live chat - liveChat: 'input#live_chat', - - // min-max + // Min-Max minMax: { minimumAmountToPlaceAnOrder: 'input#min_amount_to_order', maximumAmountToPlaceAnOrder: 'input#max_amount_to_order', @@ -6931,7 +6917,6 @@ export const selector = { price: '//div[@class="summary entry-summary"]//p[@class="price"]', quantity: 'div.quantity input.qty', addToCart: 'button.single_add_to_cart_button', - chatNow: 'button.dokan-live-chat', viewCart: '.woocommerce .woocommerce-message > .button', category: '.product_meta .posted_in', @@ -7280,10 +7265,9 @@ export const selector = { // Store Tabs storeTabs: { - follow: 'button.dokan-follow-store-button', + follow: '.dokan-follow-store-button', getSupport: 'button.dokan-store-support-btn', - chatNow: 'button.dokan-live-chat', - share: 'button.dokan-share-btn', + share: '.dokan-share-btn', products: '//div[@class="dokan-store-tabs"]//a[contains(text(),"Products")]', toc: '//div[@class="dokan-store-tabs"]//a[contains(text(),"Terms and Conditions")]', @@ -7764,16 +7748,6 @@ export const selector = { }, }, - cLiveChat: { - liveChatIframe: '(//div[ contains(@id, "__talkjs_popup_container") and not (@style="display: none;") ]//iframe[@name="____talkjs__chat__ui_internal"])[last()]', - // liveChatIframe: '(//iframe[@name="____talkjs__chat__ui_internal"])[last()]', - liveChatLauncher: 'a#__talkjs_launcher', - chatBox: 'div#chat-box', - chatTextBox: '//div[@role="textbox"]', - sendButton: 'button.send-button', - sentMessage: (message: string) => `//span[@class="EntityTreeRenderer" and normalize-space(text())="${message}"]`, - }, - cOrderReceived: { orderReceivedHeading: '//h1[normalize-space()="Order received"]', orderReceivedSuccessMessage: '.woocommerce-notice.woocommerce-notice--success.woocommerce-thankyou-order-received', diff --git a/tests/pw/pages/settingsPage.ts b/tests/pw/pages/settingsPage.ts index 199d6113ee..f5977f1a28 100644 --- a/tests/pw/pages/settingsPage.ts +++ b/tests/pw/pages/settingsPage.ts @@ -414,30 +414,6 @@ export class SettingsPage extends AdminPage { await this.toContainText(settingsAdmin.dokanUpdateSuccessMessage, quote.saveSuccessMessage); } - // Admin Set Dokan live chat Settings - async setDokanLiveChatSettings(liveChat: dokanSettings['liveChat']) { - await this.goToDokanSettings(); - await this.click(settingsAdmin.menus.liveChat); - - // liveChat Settings - await this.enableSwitcher(settingsAdmin.liveChat.enableLiveChat); - await this.click(settingsAdmin.liveChat.chatProvider(liveChat.chatProvider)); - await this.clearAndType(settingsAdmin.liveChat.talkJsAppId, liveChat.talkJsAppId); - await this.clearAndType(settingsAdmin.liveChat.talkJsAppSecret, liveChat.talkJsAppSecret); - await this.enableSwitcher(settingsAdmin.liveChat.chatButtonOnVendorPage); - await this.selectByValue(settingsAdmin.liveChat.chatButtonOnProductPage, liveChat.chatButtonPosition); - - // save settings - await this.clickAndWaitForResponseAndLoadState(data.subUrls.ajax, settingsAdmin.liveChat.liveChatSaveChanges); - - await this.toHaveBackgroundColor(settingsAdmin.liveChat.enableLiveChat + '//span', 'rgb(0, 144, 255)'); - await this.toHaveClass(settingsAdmin.liveChat.chatProvider(liveChat.chatProvider), 'checked'); - await this.toHaveValue(settingsAdmin.liveChat.talkJsAppId, liveChat.talkJsAppId); - await this.toHaveValue(settingsAdmin.liveChat.talkJsAppSecret, liveChat.talkJsAppSecret); - await this.toHaveBackgroundColor(settingsAdmin.liveChat.chatButtonOnVendorPage + '//span', 'rgb(0, 144, 255)'); - await this.toHaveSelectedValue(settingsAdmin.liveChat.chatButtonOnProductPage, liveChat.chatButtonPosition); - } - // Admin Set Dokan Rma Settings async setDokanRmaSettings(rma: dokanSettings['rma']) { await this.goToDokanSettings(); diff --git a/tests/pw/pages/singleProductPage.ts b/tests/pw/pages/singleProductPage.ts index 951147d02b..029634ebae 100644 --- a/tests/pw/pages/singleProductPage.ts +++ b/tests/pw/pages/singleProductPage.ts @@ -21,7 +21,7 @@ export class SingleProductPage extends CustomerPage { await this.goToProductDetails(productName); // basic details are visible - const { viewCart, chatNow, euComplianceData, productAddedSuccessMessage, productWithQuantityAddedSuccessMessage, ...productDetails } = singleProductCustomer.productDetails; + const { viewCart, euComplianceData, productAddedSuccessMessage, productWithQuantityAddedSuccessMessage, ...productDetails } = singleProductCustomer.productDetails; await this.multipleElementVisible(productDetails); // description elements are visible diff --git a/tests/pw/pages/singleStorePage.ts b/tests/pw/pages/singleStorePage.ts index d7c6efb460..4f280c7f77 100644 --- a/tests/pw/pages/singleStorePage.ts +++ b/tests/pw/pages/singleStorePage.ts @@ -30,7 +30,7 @@ export class SingleStorePage extends CustomerPage { await this.toBeVisible(singleStoreCustomer.storeTabs.products); // await this.toBeVisible(singleStoreCustomer.storeTabs.toc); // todo: need vendor toc } else { - const { toc, chatNow, ...storeTabs } = singleStoreCustomer.storeTabs; + const { toc, ...storeTabs } = singleStoreCustomer.storeTabs; await this.multipleElementVisible(storeTabs); // eu compliance data is visible diff --git a/tests/pw/pages/storeSupportsPage.ts b/tests/pw/pages/storeSupportsPage.ts index a85c3bf632..ef1a546aa5 100644 --- a/tests/pw/pages/storeSupportsPage.ts +++ b/tests/pw/pages/storeSupportsPage.ts @@ -424,7 +424,7 @@ export class StoreSupportsPage extends AdminPage { await this.toBeVisible(supportsTicketsCustomer.supportTicketDetails.orderReference.orderReferenceLink(orderId)); } - // customer send message to support ticket + // customer send message to support ticket async sendMessageToSupportTicket(supportTicketId: string, supportTicket: customer['supportTicket']): Promise { const message = supportTicket.message(); await this.goIfNotThere(data.subUrls.frontend.supportTicketDetails(supportTicketId)); diff --git a/tests/pw/pages/vendorSettingsPage.ts b/tests/pw/pages/vendorSettingsPage.ts index a63feca8e8..69cda561f5 100644 --- a/tests/pw/pages/vendorSettingsPage.ts +++ b/tests/pw/pages/vendorSettingsPage.ts @@ -236,10 +236,6 @@ export class VendorSettingsPage extends VendorPage { await this.storeSupportSettings(vendorInfo.supportButtonText); break; - case 'liveChat': - await this.liveChatSettings(vendorInfo.liveChat); - break; - case 'min-max': await this.minMaxSettings(vendorInfo.minMax); break; @@ -427,11 +423,6 @@ export class VendorSettingsPage extends VendorPage { } } - // vendor set liveChat settings - async liveChatSettings(liveChat: vendor['vendorInfo']['liveChat']): Promise { - await this.check(settingsVendor.liveChat); - } - // vendor set minmax settings async minMaxSettings(minMax: vendor['vendorInfo']['minMax']): Promise { await this.clearAndType(settingsVendor.minMax.minimumAmountToPlaceAnOrder, minMax.minimumAmount); diff --git a/tests/pw/tests/e2e/_env.setup.ts b/tests/pw/tests/e2e/_env.setup.ts index 2813f2706e..7d9e28b771 100644 --- a/tests/pw/tests/e2e/_env.setup.ts +++ b/tests/pw/tests/e2e/_env.setup.ts @@ -229,10 +229,6 @@ setup.describe('setup dokan settings', () => { await dbUtils.setOptionValue(dbData.dokan.optionName.quote, dbData.dokan.quoteSettings); }); - setup('admin set dokan live chat settings', { tag: ['@pro'] }, async () => { - await dbUtils.setOptionValue(dbData.dokan.optionName.liveChat, dbData.dokan.liveChatSettings); - }); - setup('admin set dokan rma settings', { tag: ['@pro'] }, async () => { await dbUtils.setOptionValue(dbData.dokan.optionName.rma, dbData.dokan.rmaSettings); }); diff --git a/tests/pw/tests/e2e/liveChat.spec.ts b/tests/pw/tests/e2e/liveChat.spec.ts deleted file mode 100644 index 37d988ee4d..0000000000 --- a/tests/pw/tests/e2e/liveChat.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { test, Page } from '@playwright/test'; -import { LiveChatPage } from '@pages/liveChatPage'; -import { dbUtils } from '@utils/dbUtils'; -import { data } from '@utils/testData'; -import { dbData } from '@utils/dbData'; - -test.describe('Live chat test', () => { - let vendor: LiveChatPage; - let customer: LiveChatPage; - let vPage: Page, cPage: Page; - - test.beforeAll(async ({ browser }) => { - const vendorContext = await browser.newContext(data.auth.vendorAuth); - vPage = await vendorContext.newPage(); - vendor = new LiveChatPage(vPage); - - const customerContext = await browser.newContext(data.auth.customerAuth); - cPage = await customerContext.newPage(); - customer = new LiveChatPage(cPage); - - // todo: enable vendor live chat - }); - - test.afterAll(async () => { - await dbUtils.setOptionValue(dbData.dokan.optionName.liveChat, dbData.dokan.liveChatSettings); - await cPage.close(); - }); - - // admin - - test('admin can enable chat button on vendor page', { tag: ['@pro', '@admin'] }, async () => { - await dbUtils.updateOptionValue(dbData.dokan.optionName.liveChat, { chat_button_product_page: 'on' }); - await customer.viewLiveChatButtonOnStore(data.predefined.vendorInfo.shopName); - }); - - test('admin can disable chat button on vendor page', { tag: ['@pro', '@admin'] }, async () => { - test.skip(true, 'Has Dokan Issues'); - await dbUtils.updateOptionValue(dbData.dokan.optionName.liveChat, { chat_button_product_page: 'off' }); - await customer.viewLiveChatButtonOnStore(data.predefined.vendorInfo.shopName, true); - // reset - await dbUtils.updateOptionValue(dbData.dokan.optionName.liveChat, { chat_button_product_page: 'on' }); - }); - - test('admin can enable chat button on product page (above_tab)', { tag: ['@pro', '@admin'] }, async () => { - await dbUtils.updateOptionValue(dbData.dokan.optionName.liveChat, { chat_button_product_page: 'above_tab' }); - await customer.viewLiveChatButtonOnProduct(data.predefined.simpleProduct.product1.name, 'above-tab'); - }); - - test('admin can enable chat button on product page (inside_tab)', { tag: ['@pro', '@admin'] }, async () => { - test.skip(true, 'Has Dokan Issues'); - await dbUtils.updateOptionValue(dbData.dokan.optionName.liveChat, { chat_button_product_page: 'inside_tab' }); - await customer.viewLiveChatButtonOnProduct(data.predefined.simpleProduct.product1.name, 'inside-tab'); - }); - - test('admin can disable chat button on product page', { tag: ['@pro', '@admin'] }, async () => { - test.skip(true, 'Has Dokan Issues'); - await dbUtils.updateOptionValue(dbData.dokan.optionName.liveChat, { chat_button_product_page: 'dont_show' }); - await customer.viewLiveChatButtonOnProduct(data.predefined.simpleProduct.product1.name, 'dont-show'); - }); - - // vendor - - test('vendor can view inbox menu page', { tag: ['@pro', '@exploratory', '@vendor'] }, async () => { - await vendor.vendorInboxRenderProperly(); - }); - - test('vendor can reply to customer message', { tag: ['@pro', '@customer'] }, async () => { - await customer.sendMessageToVendor('vendor1store', data.uniqueId.nanoIdRandom()); - await vendor.sendMessageToCustomer(data.predefined.customerInfo.username1, data.uniqueId.nanoIdRandom()); - }); - - // customer - - test('customer can send message to vendor', { tag: ['@pro', '@customer'] }, async () => { - await customer.sendMessageToVendor(data.predefined.vendorInfo.shopName, data.uniqueId.nanoIdRandom()); - }); -}); diff --git a/tests/pw/tests/e2e/settings.spec.ts b/tests/pw/tests/e2e/settings.spec.ts index 7edaf5096c..d45586758b 100644 --- a/tests/pw/tests/e2e/settings.spec.ts +++ b/tests/pw/tests/e2e/settings.spec.ts @@ -95,10 +95,6 @@ test.describe('Settings test', () => { await admin.setDokanQuoteSettings(data.dokanSettings.quote); }); - test('admin can set Dokan live chat settings', { tag: ['@pro', '@admin'] }, async () => { - await admin.setDokanLiveChatSettings(data.dokanSettings.liveChat); - }); - test('admin can set Dokan rma settings', { tag: ['@pro', '@admin'] }, async () => { await admin.setDokanRmaSettings(data.dokanSettings.rma); }); diff --git a/tests/pw/tests/e2e/vendorSettings.spec.ts b/tests/pw/tests/e2e/vendorSettings.spec.ts index 611624476a..57c3fc151c 100644 --- a/tests/pw/tests/e2e/vendorSettings.spec.ts +++ b/tests/pw/tests/e2e/vendorSettings.spec.ts @@ -106,10 +106,6 @@ test.describe('Vendor settings test', () => { await vendor.setStoreSettings(data.vendor.vendorInfo, 'store-support'); }); - test('vendor can set live chat settings', { tag: ['@pro', '@vendor'] }, async () => { - await vendor.setStoreSettings(data.vendor.vendorInfo, 'liveChat'); - }); - test('vendor can set min-max settings', { tag: ['@pro', '@vendor'] }, async () => { await vendor.setStoreSettings(data.vendor.vendorInfo, 'min-max'); // disable min-max diff --git a/tests/pw/types/environment.d.ts b/tests/pw/types/environment.d.ts index a3915c282d..0774a54883 100644 --- a/tests/pw/types/environment.d.ts +++ b/tests/pw/types/environment.d.ts @@ -26,8 +26,6 @@ declare global { GMAP: string; MAPBOX: string; LICENSE_KEY: string; - TALKJS_APP_ID: string; - TALKJS_APP_SECRET: string; DOKAN_PRO: boolean; SITE_LANGUAGE: string; SITE_TITLE: string; diff --git a/tests/pw/utils/dbData.ts b/tests/pw/utils/dbData.ts index 612b11e22e..f3cea8a3bd 100644 --- a/tests/pw/utils/dbData.ts +++ b/tests/pw/utils/dbData.ts @@ -1,4 +1,4 @@ -const { BASE_URL, GMAP, MAPBOX, LICENSE_KEY, TALKJS_APP_ID, TALKJS_APP_SECRET } = process.env; +const { BASE_URL, GMAP, MAPBOX, LICENSE_KEY } = process.env; export const dbData = { dokan: { @@ -20,7 +20,7 @@ export const dbData = { // socialApi: 'dokan_social_api', shippingStatus: 'dokan_shipping_status_setting', quote: 'dokan_quote_settings', - liveChat: 'dokan_live_chat', + // liveChat: 'dokan_live_chat', rma: 'dokan_rma', wholesale: 'dokan_wholesale', euCompliance: 'dokan_germanized', @@ -897,16 +897,15 @@ export const dbData = { }, liveChatSettings: { - enable: 'on', - provider: 'talkjs', + enable: 'off', + provider: 'messenger', theme_color: '#0084FF', - app_id: TALKJS_APP_ID, - app_secret: TALKJS_APP_SECRET, + app_id: '', + app_secret: '', wa_opening_method: 'in_app', wa_pre_filled_message: 'Hello {store_name}, I have an enquiry regarding your store at {store_url}', chat_button_seller_page: 'on', - chat_button_product_page: 'above_tab', // above_tab, inside_tab, dont_show - dashboard_menu_manager: [], + chat_button_product_page: 'above_tab', }, rmaSettings: { diff --git a/tests/pw/utils/interfaces.ts b/tests/pw/utils/interfaces.ts index 2ee333ca59..de232d85d3 100644 --- a/tests/pw/utils/interfaces.ts +++ b/tests/pw/utils/interfaces.ts @@ -815,10 +815,6 @@ export interface vendor { discountPercentage: string; }; - liveChat: { - pageId: string; - }; - minMax: { minimumProductQuantity: string; maximumProductQuantity: string; @@ -1696,16 +1692,6 @@ export interface dokanSettings { saveSuccessMessage: string; }; - // Rma Settings - liveChat: { - settingTitle: string; - chatProvider: string; - talkJsAppId: string; - talkJsAppSecret: string; - chatButtonPosition: string; - saveSuccessMessage: string; - }; - // Rma Settings rma: { settingTitle: string; diff --git a/tests/pw/utils/testData.ts b/tests/pw/utils/testData.ts index 6c852c6dfc..0eca4b12d8 100644 --- a/tests/pw/utils/testData.ts +++ b/tests/pw/utils/testData.ts @@ -23,8 +23,6 @@ const { GMAP, MAPBOX, LICENSE_KEY, - TALKJS_APP_ID, - TALKJS_APP_SECRET, } = process.env; const basicAuth = (username: string, password: string) => 'Basic ' + Buffer.from(username + ':' + password).toString('base64'); @@ -1006,10 +1004,10 @@ export const data = { storeListingSort: 'store-listing/?stores_orderby', cart: 'cart', checkout: 'checkout', - addToCart: 'wc-ajax=add_to_cart', - applyCoupon: 'wc-ajax=apply_coupon', - removeCoupon: 'wc-ajax=remove_coupon', - refreshedFragment: 'wc-ajax=get_refreshed_fragments', + addToCart: '?wc-ajax=add_to_cart', + applyCoupon: '?wc-ajax=apply_coupon', + removeCoupon: '?wc-ajax=remove_coupon', + refreshedFragment: '?wc-ajax=get_refreshed_fragments', placeOrder: '?wc-ajax=checkout', billingAddress: 'my-account/edit-address/billing', shippingAddress: 'my-account/edit-address/shipping', @@ -1026,7 +1024,6 @@ export const data = { quoteDetails: (quotId: string) => `my-account/request-a-quote/${quotId}`, supportTicketDetails: (ticketId: string) => `my-account/support-tickets/${ticketId}`, productSubscriptionDetails: (subscriptionId: string) => `my-account/view-subscription/${subscriptionId}`, - talkjs: 'app.talkjs.com/api', productReview: 'wp-comments-post.php', submitSupport: 'wp-comments-post.php', @@ -1070,7 +1067,6 @@ export const data = { csvExport: 'dashboard/tools/csv-export', auction: 'dashboard/auction', auctionActivity: 'dashboard/auction-activity', - inbox: 'dashboard/inbox', storeSupport: 'dashboard/support', // sub menus @@ -1078,7 +1074,6 @@ export const data = { settingsAddon: 'dashboard/settings/product-addon', settingsAddonEdit: (addonId: string) => `dashboard/settings/product-addon/?edit=${addonId}`, settingsPayment: 'dashboard/settings/payment', - // payment settings paypal: 'dashboard/settings/payment-manage-paypal', bankTransfer: 'dashboard/settings/payment-manage-bank', @@ -1294,10 +1289,6 @@ export const data = { discountPercentage: '10', }, - liveChat: { - pageId: '', - }, - minMax: { minimumProductQuantity: '1', maximumProductQuantity: '20', @@ -2315,16 +2306,6 @@ export const data = { saveSuccessMessage: 'Setting has been saved successfully.', }, - // live chat - liveChat: { - settingTitle: 'Live Chat Settings', - chatProvider: 'talkjs', // messenger, talkjs, tawkto, whatsapp - talkJsAppId: TALKJS_APP_ID, - talkJsAppSecret: TALKJS_APP_SECRET, - chatButtonPosition: 'above_tab', // above_tab, inside_tab, dont_show - saveSuccessMessage: 'Setting has been saved successfully.', - }, - // Rma Settings rma: { settingTitle: 'RMA Settings', @@ -2469,7 +2450,6 @@ export const data = { uniqueId: { uuid: faker.string.uuid(), nanoId: faker.string.nanoid(10), - nanoIdRandom: () => faker.string.nanoid(10), }, // predefined test data From d3f99ec2d4226aef4cd1ab019c4a7b7c3184c785 Mon Sep 17 00:00:00 2001 From: Mahbub Rabbani Date: Mon, 28 Oct 2024 12:07:27 +0600 Subject: [PATCH 05/12] Refactor/introduce container (#2312) * Implement league container to swap Dokan curent container * Seperate file for WeDevs_Dokan class * Update Docblocks * Fix plugin activation hooks * Add vendor hooks * Remove un-related service provider * Add container documentation --- composer.json | 3 +- docs/container.md | 129 ++++ dokan-class.php | 482 +++++++++++++++ dokan.php | 557 +----------------- includes/Contracts/Hookable.php | 19 + .../BaseServiceProvider.php | 79 +++ .../BootableServiceProvider.php | 19 + includes/DependencyManagement/Container.php | 16 + .../ContainerException.php | 23 + includes/DependencyManagement/Definition.php | 70 +++ .../Providers/AdminServiceProvider.php | 58 ++ .../Providers/AjaxServiceProvider.php | 34 ++ .../Providers/CommonServiceProvider.php | 66 +++ .../Providers/FrontendServiceProvider.php | 38 ++ .../Providers/ServiceProvider.php | 77 +++ .../Container/Argument/ArgumentInterface.php | 13 + .../Argument/ArgumentResolverInterface.php | 14 + .../Argument/ArgumentResolverTrait.php | 111 ++++ .../Argument/DefaultValueArgument.php | 24 + .../Argument/DefaultValueInterface.php | 13 + .../Argument/Literal/ArrayArgument.php | 15 + .../Argument/Literal/BooleanArgument.php | 15 + .../Argument/Literal/CallableArgument.php | 15 + .../Argument/Literal/FloatArgument.php | 15 + .../Argument/Literal/IntegerArgument.php | 15 + .../Argument/Literal/ObjectArgument.php | 15 + .../Argument/Literal/StringArgument.php | 15 + .../Container/Argument/LiteralArgument.php | 48 ++ .../Argument/LiteralArgumentInterface.php | 9 + .../Container/Argument/ResolvableArgument.php | 20 + .../Argument/ResolvableArgumentInterface.php | 10 + lib/packages/League/Container/Container.php | 210 +++++++ .../Container/ContainerAwareInterface.php | 11 + .../League/Container/ContainerAwareTrait.php | 40 ++ .../Container/Definition/Definition.php | 238 ++++++++ .../Definition/DefinitionAggregate.php | 117 ++++ .../DefinitionAggregateInterface.php | 21 + .../Definition/DefinitionInterface.php | 25 + .../DefinitionContainerInterface.php | 20 + .../Exception/ContainerException.php | 12 + .../Container/Exception/NotFoundException.php | 12 + .../League/Container/Inflector/Inflector.php | 97 +++ .../Inflector/InflectorAggregate.php | 44 ++ .../Inflector/InflectorAggregateInterface.php | 14 + .../Inflector/InflectorInterface.php | 15 + .../League/Container/ReflectionContainer.php | 107 ++++ .../AbstractServiceProvider.php | 28 + .../BootableServiceProviderInterface.php | 16 + .../ServiceProviderAggregate.php | 76 +++ .../ServiceProviderAggregateInterface.php | 15 + .../ServiceProviderInterface.php | 15 + .../Container/ContainerExceptionInterface.php | 12 + .../Psr/Container/ContainerInterface.php | 36 ++ .../Container/NotFoundExceptionInterface.php | 10 + 54 files changed, 2695 insertions(+), 533 deletions(-) create mode 100644 docs/container.md create mode 100755 dokan-class.php create mode 100644 includes/Contracts/Hookable.php create mode 100644 includes/DependencyManagement/BaseServiceProvider.php create mode 100644 includes/DependencyManagement/BootableServiceProvider.php create mode 100644 includes/DependencyManagement/Container.php create mode 100644 includes/DependencyManagement/ContainerException.php create mode 100644 includes/DependencyManagement/Definition.php create mode 100644 includes/DependencyManagement/Providers/AdminServiceProvider.php create mode 100644 includes/DependencyManagement/Providers/AjaxServiceProvider.php create mode 100644 includes/DependencyManagement/Providers/CommonServiceProvider.php create mode 100644 includes/DependencyManagement/Providers/FrontendServiceProvider.php create mode 100644 includes/DependencyManagement/Providers/ServiceProvider.php create mode 100644 lib/packages/League/Container/Argument/ArgumentInterface.php create mode 100644 lib/packages/League/Container/Argument/ArgumentResolverInterface.php create mode 100644 lib/packages/League/Container/Argument/ArgumentResolverTrait.php create mode 100644 lib/packages/League/Container/Argument/DefaultValueArgument.php create mode 100644 lib/packages/League/Container/Argument/DefaultValueInterface.php create mode 100644 lib/packages/League/Container/Argument/Literal/ArrayArgument.php create mode 100644 lib/packages/League/Container/Argument/Literal/BooleanArgument.php create mode 100644 lib/packages/League/Container/Argument/Literal/CallableArgument.php create mode 100644 lib/packages/League/Container/Argument/Literal/FloatArgument.php create mode 100644 lib/packages/League/Container/Argument/Literal/IntegerArgument.php create mode 100644 lib/packages/League/Container/Argument/Literal/ObjectArgument.php create mode 100644 lib/packages/League/Container/Argument/Literal/StringArgument.php create mode 100644 lib/packages/League/Container/Argument/LiteralArgument.php create mode 100644 lib/packages/League/Container/Argument/LiteralArgumentInterface.php create mode 100644 lib/packages/League/Container/Argument/ResolvableArgument.php create mode 100644 lib/packages/League/Container/Argument/ResolvableArgumentInterface.php create mode 100644 lib/packages/League/Container/Container.php create mode 100644 lib/packages/League/Container/ContainerAwareInterface.php create mode 100644 lib/packages/League/Container/ContainerAwareTrait.php create mode 100644 lib/packages/League/Container/Definition/Definition.php create mode 100644 lib/packages/League/Container/Definition/DefinitionAggregate.php create mode 100644 lib/packages/League/Container/Definition/DefinitionAggregateInterface.php create mode 100644 lib/packages/League/Container/Definition/DefinitionInterface.php create mode 100644 lib/packages/League/Container/DefinitionContainerInterface.php create mode 100644 lib/packages/League/Container/Exception/ContainerException.php create mode 100644 lib/packages/League/Container/Exception/NotFoundException.php create mode 100644 lib/packages/League/Container/Inflector/Inflector.php create mode 100644 lib/packages/League/Container/Inflector/InflectorAggregate.php create mode 100644 lib/packages/League/Container/Inflector/InflectorAggregateInterface.php create mode 100644 lib/packages/League/Container/Inflector/InflectorInterface.php create mode 100644 lib/packages/League/Container/ReflectionContainer.php create mode 100644 lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php create mode 100644 lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php create mode 100644 lib/packages/League/Container/ServiceProvider/ServiceProviderAggregate.php create mode 100644 lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php create mode 100644 lib/packages/League/Container/ServiceProvider/ServiceProviderInterface.php create mode 100644 lib/packages/Psr/Container/ContainerExceptionInterface.php create mode 100644 lib/packages/Psr/Container/ContainerInterface.php create mode 100644 lib/packages/Psr/Container/NotFoundExceptionInterface.php diff --git a/composer.json b/composer.json index bfcbbb1e57..558bc241a7 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ }, "autoload": { "psr-4": { - "WeDevs\\Dokan\\": "includes/" + "WeDevs\\Dokan\\": "includes/", + "WeDevs\\Dokan\\ThirdParty\\Packages\\": "lib/packages/" }, "files": [ "includes/functions-rest-api.php", diff --git a/docs/container.md b/docs/container.md new file mode 100644 index 0000000000..9a4caea40d --- /dev/null +++ b/docs/container.md @@ -0,0 +1,129 @@ +## Container Documentation + +- [Register Service Provider](#register-service-provider) +- [Register Services in the Service Provider](#register-services-in-the-service-provider) +- [Add Services to the Container](#add-services-to-the-container) +- [Get Service from the Container](#get-service-from-the-container) +- [Override the Existing Service](#override-the-existing-service) +- [Check if a Service is Registered](#check-service-is-registered-or-not) + +### Register Service Provider + +1. **Step 1:** Create a service provider inside `includes/DependencyManagement/Providers` that extends `WeDevs\Dokan\DependencyManagement\BaseServiceProvider`. +2. **Step 2:** Register the service provider in the `boot` method of `includes/DependencyManagement/Providers/ServiceProvider.php`. + +You can see the already registered service providers inside the `boot` method of the [ServiceProvider](./../includes/DependencyManagement/Providers/ServiceProvider.php#L46) class. + +### Register Services in the Service Provider + +1. **Step 1:** Register the services inside the `register` method of your service provider. +2. **Step 2:** Implement a `provides` method that returns `true` or `false` when the container invokes it with a service name. + +```php +namespace WeDevs\Dokan\DependencyManagement\Providers; + +use WeDevs\Dokan\DependencyManagement\BaseServiceProvider; + +class SomeServiceProvider extends BaseServiceProvider +{ + /** + * The provides method lets the container know + * which services are provided by this provider. + * The alias must be added to this array or it will + * be ignored. + */ + public function provides(string $id): bool + { + $services = [ + 'key', + Some\Controller::class, + Some\Model::class, + Some\Request::class, + ]; + + return in_array($id, $services); + } + + /** + * The register method defines services in the container. + * Services must have an alias in the `provides` method + * or they will be ignored. + */ + public function register(): void + { + $this->getContainer()->add('key', 'value'); + + $this->getContainer() + ->add(Some\Controller::class) + ->addArgument(Some\Request::class) + ->addArgument(Some\Model::class); + + $this->getContainer()->add(Some\Request::class); + $this->getContainer()->add(Some\Model::class); + } +} +``` + +### Add Services to the Container + +- Add a service: + +```php +$this->getContainer()->add(ServiceClass::class); +``` + +- Add a shared service (one instance per request lifecycle): + +```php +$this->getContainer()->addShared(ServiceClass::class); +``` + +- Add a service with constructor parameters: + +```php +$this->getContainer()->addShared(ServiceClass::class, function () { + return new ServiceClass($params); +}); +``` + +- Add a shared service with constructor parameters and tag it: + +```php +$this->getContainer()->addShared(ServiceClass::class, function () { + return new ServiceClass($params); +})->addTag('tag_name'); +``` + +- Add a service and tag all implemented interfaces: + +```php +$this->getContainer()->share_with_implements_tags(ServiceClass::class); +``` + +### Get Service from the Container + +- Get a single instance: + +```php +$service = dokan()->get_container()->get(ServiceClass::class); +``` + +- Get an array of instances by tag: + +```php +$service_list = dokan()->get_container()->get('tag-name'); +``` + +### Override the Existing Service + +```php +dokan()->get_container()->extend(ServiceClass::class)->setConcrete(new OtherServiceClass()); +``` + +### Check if a Service is Registered + +```php +$is_registered = dokan()->get_container()->has(ServiceClass::class); +``` + +For more details, visit the [League Container documentation](https://container.thephpleague.com/4.x/service-providers). \ No newline at end of file diff --git a/dokan-class.php b/dokan-class.php new file mode 100755 index 0000000000..12392395ad --- /dev/null +++ b/dokan-class.php @@ -0,0 +1,482 @@ +define_constants(); + + register_activation_hook( DOKAN_FILE, [ $this, 'activate' ] ); + register_deactivation_hook( DOKAN_FILE, [ $this, 'deactivate' ] ); + + add_action( 'before_woocommerce_init', [ $this, 'declare_woocommerce_feature_compatibility' ] ); + add_action( 'woocommerce_loaded', [ $this, 'init_plugin' ] ); + add_action( 'woocommerce_flush_rewrite_rules', [ $this, 'flush_rewrite_rules' ] ); + + // Register admin notices to container and load notices + $this->get_container()->get( 'admin_notices' ); + + $this->init_appsero_tracker(); + + add_action( 'plugins_loaded', [ $this, 'woocommerce_not_loaded' ], 11 ); + } + + /** + * Initializes the WeDevs_Dokan() class + * + * Checks for an existing WeDevs_WeDevs_Dokan() instance + * and if it doesn't find one, create it. + */ + public static function init() { + if ( self::$instance === null ) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Magic getter to bypass referencing objects + * + * @since 2.6.10 + * + * @param string $prop + * + * @return object Class Instance + */ + public function __get( $prop ) { + if ( $this->get_container()->has( $prop ) ) { + return $this->get_container()->get( $prop ); + } + + if ( array_key_exists( $prop, $this->legacy_container ) ) { + return $this->legacy_container[ $prop ]; + } + } + + /** + * Check if the PHP version is supported + * + * @return bool + */ + public function is_supported_php() { + if ( version_compare( PHP_VERSION, $this->min_php, '<=' ) ) { + return false; + } + + return true; + } + + /** + * Get the plugin path. + * + * @return string + */ + public function plugin_path() { + return untrailingslashit( plugin_dir_path( __FILE__ ) ); + } + + /** + * Get the template path. + * + * @return string + */ + public function template_path() { + return apply_filters( 'dokan_template_path', 'dokan/' ); + } + + /** + * Placeholder for activation function + * + * Nothing being called here yet. + */ + public function activate() { + if ( ! $this->has_woocommerce() ) { + set_transient( 'dokan_wc_missing_notice', true ); + } + + if ( ! $this->is_supported_php() ) { + require_once WC_ABSPATH . 'includes/wc-notice-functions.php'; + + /* translators: 1: Required PHP Version 2: Running php version */ + wc_print_notice( sprintf( __( 'The Minimum PHP Version Requirement for Dokan is %1$s. You are Running PHP %2$s', 'dokan-lite' ), $this->min_php, phpversion() ), 'error' ); + exit; + } + + require_once __DIR__ . '/includes/functions.php'; + require_once __DIR__ . '/includes/functions-compatibility.php'; + + $this->get_container()->get( 'upgrades' ); + $installer = new \WeDevs\Dokan\Install\Installer(); + $installer->do_install(); + + // rewrite rules during dokan activation + if ( $this->has_woocommerce() ) { + $this->flush_rewrite_rules(); + } + } + + /** + * Flush rewrite rules after dokan is activated or woocommerce is activated + * + * @since 3.2.8 + */ + public function flush_rewrite_rules() { + // fix rewrite rules + $this->get_container()->get( 'rewrite' )->register_rule(); + flush_rewrite_rules(); + } + + /** + * Placeholder for deactivation function + * + * Nothing being called here yet. + */ + public function deactivate() { + delete_transient( 'dokan_wc_missing_notice', true ); + } + + /** + * Initialize plugin for localization + * + * @uses load_plugin_textdomain() + */ + public function localization_setup() { + load_plugin_textdomain( 'dokan-lite', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' ); + } + + /** + * Define all constants + * + * @return void + */ + public function define_constants() { + defined( 'DOKAN_PLUGIN_VERSION' ) || define( 'DOKAN_PLUGIN_VERSION', $this->version ); + defined( 'DOKAN_DIR' ) || define( 'DOKAN_DIR', __DIR__ ); + defined( 'DOKAN_INC_DIR' ) || define( 'DOKAN_INC_DIR', __DIR__ . '/includes' ); + defined( 'DOKAN_LIB_DIR' ) || define( 'DOKAN_LIB_DIR', __DIR__ . '/lib' ); + defined( 'DOKAN_PLUGIN_ASSEST' ) || define( 'DOKAN_PLUGIN_ASSEST', plugins_url( 'assets', __FILE__ ) ); + + // give a way to turn off loading styles and scripts from parent theme + defined( 'DOKAN_LOAD_STYLE' ) || define( 'DOKAN_LOAD_STYLE', true ); + defined( 'DOKAN_LOAD_SCRIPTS' ) || define( 'DOKAN_LOAD_SCRIPTS', true ); + } + + /** + * Add High Performance Order Storage Support + * + * @since 3.8.0 + * + * @return void + */ + public function declare_woocommerce_feature_compatibility() { + if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) { + \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); + \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true ); + } + } + + /** + * Load the plugin after WP User Frontend is loaded + * + * @return void + */ + public function init_plugin() { + $this->includes(); + $this->init_hooks(); + + do_action( 'dokan_loaded' ); + } + + /** + * Initialize the actions + * + * @return void + */ + public function init_hooks() { + // Localize our plugin + add_action( 'init', [ $this, 'localization_setup' ] ); + + // initialize the classes + add_action( 'init', [ $this, 'init_classes' ], 4 ); + add_action( 'init', [ $this, 'wpdb_table_shortcuts' ], 1 ); + + add_action( 'plugins_loaded', [ $this, 'after_plugins_loaded' ] ); + + add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), [ $this, 'plugin_action_links' ] ); + add_action( 'in_plugin_update_message-dokan-lite/dokan.php', [ \WeDevs\Dokan\Install\Installer::class, 'in_plugin_update_message' ] ); + + add_action( 'widgets_init', [ $this, 'register_widgets' ] ); + } + + /** + * Include all the required files + * + * @return void + */ + public function includes() { + require_once DOKAN_DIR . '/deprecated/deprecated-functions.php'; + require_once DOKAN_DIR . '/deprecated/deprecated-hooks.php'; + require_once DOKAN_INC_DIR . '/functions.php'; + + if ( ! function_exists( 'dokan_pro' ) ) { + require_once DOKAN_INC_DIR . '/reports.php'; + } + + require_once DOKAN_INC_DIR . '/Order/functions.php'; + require_once DOKAN_INC_DIR . '/Product/functions.php'; + require_once DOKAN_INC_DIR . '/Withdraw/functions.php'; + require_once DOKAN_INC_DIR . '/functions-compatibility.php'; + require_once DOKAN_INC_DIR . '/wc-functions.php'; + + require_once DOKAN_INC_DIR . '/wc-template.php'; + require_once DOKAN_DIR . '/deprecated/deprecated-classes.php'; + + if ( is_admin() ) { + require_once DOKAN_INC_DIR . '/Admin/functions.php'; + } else { + require_once DOKAN_INC_DIR . '/template-tags.php'; + } + + require_once DOKAN_INC_DIR . '/store-functions.php'; + } + + /** + * Init all the classes + * + * @return void + */ + public function init_classes() { + $common_services = $this->get_container()->get( 'common-service' ); + + if ( is_admin() ) { + $admin_services = $this->get_container()->get( 'admin-service' ); + } else { + $frontend_services = $this->get_container()->get( 'frontend-service' ); + } + + $container_services = $this->get_container()->get( 'container-service' ); + + $this->legacy_container = apply_filters( 'dokan_get_class_container', $this->legacy_container ); + + if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { + $ajax_services = $this->get_container()->get( 'ajax-service' ); + } + } + + /** + * Load table prefix for withdraw and orders table + * + * @since 1.0 + * + * @return void + */ + public function wpdb_table_shortcuts() { + global $wpdb; + + $wpdb->dokan_withdraw = $wpdb->prefix . 'dokan_withdraw'; + $wpdb->dokan_orders = $wpdb->prefix . 'dokan_orders'; + $wpdb->dokan_announcement = $wpdb->prefix . 'dokan_announcement'; + $wpdb->dokan_refund = $wpdb->prefix . 'dokan_refund'; + $wpdb->dokan_vendor_balance = $wpdb->prefix . 'dokan_vendor_balance'; + } + + /** + * Executed after all plugins are loaded + * + * At this point Dokan Pro is loaded + * + * @since 2.8.7 + * + * @return void + */ + public function after_plugins_loaded() { + // Initiate background processes + $processes = get_option( 'dokan_background_processes', [] ); + + if ( ! empty( $processes ) ) { + $update = false; + foreach ( $processes as $processor => $file ) { + if ( file_exists( $file ) ) { + include_once $file; + new $processor(); + } else { + $update = true; + unset( $processes[ $processor ] ); + } + } + if ( $update ) { + update_option( 'dokan_background_processes', $processes ); + } + } + } + + /** + * Register widgets + * + * @since 2.8 + * + * @return void + */ + public function register_widgets() { + $this->get_container()->get( 'widgets' ); + } + + /** + * Returns if the plugin is in PRO version + * + * @since 2.4 + * + * @return bool + */ + public function is_pro_exists() { + return apply_filters( 'dokan_is_pro_exists', false ); + } + + /** + * Plugin action links + * + * @param array $links + * + * @since 2.4 + * + * @return array + */ + public function plugin_action_links( $links ) { + if ( ! $this->is_pro_exists() ) { + $links[] = '' . __( 'Get Pro', 'dokan-lite' ) . ''; + } + + $links[] = '' . __( 'Settings', 'dokan-lite' ) . ''; + $links[] = '' . __( 'Documentation', 'dokan-lite' ) . ''; + + return $links; + } + + /** + * Initialize Appsero Tracker + * + * @return void + */ + public function init_appsero_tracker() { + $this->get_container()->get( 'tracker' ); + } + + /** + * Check whether woocommerce is installed and active + * + * @since 2.9.16 + * + * @return bool + */ + public function has_woocommerce() { + return class_exists( 'WooCommerce' ); + } + + /** + * Check whether woocommerce is installed + * + * @since 3.2.8 + * + * @return bool + */ + public function is_woocommerce_installed() { + return in_array( 'woocommerce/woocommerce.php', array_keys( get_plugins() ), true ); + } + + /** + * Handles scenerios when WooCommerce is not active + * + * @since 2.9.27 + * + * @return void + */ + public function woocommerce_not_loaded() { + if ( did_action( 'woocommerce_loaded' ) || ! is_admin() ) { + return; + } + + require_once DOKAN_INC_DIR . '/functions.php'; + + if ( get_transient( '_dokan_setup_page_redirect' ) ) { + dokan_redirect_to_admin_setup_wizard(); + } + + new \WeDevs\Dokan\Admin\SetupWizardNoWC(); + } + + /** + * Get Dokan db version key + * + * @since 3.0.0 + * + * @return string + */ + public function get_db_version_key() { + return $this->db_version_key; + } + + public function get_container(): Container { + return dokan_get_container(); + } +} diff --git a/dokan.php b/dokan.php index 521eee6133..4aa8a5f0d8 100755 --- a/dokan.php +++ b/dokan.php @@ -45,551 +45,44 @@ exit; } -/** - * WeDevs_Dokan class - * - * @class WeDevs_Dokan The class that holds the entire WeDevs_Dokan plugin - * - * @property WeDevs\Dokan\Commission $commission Instance of Commission class - * @property WeDevs\Dokan\Order\Manager $order Instance of Order Manager class - * @property WeDevs\Dokan\Product\Manager $product Instance of Order Manager class - * @property WeDevs\Dokan\Vendor\Manager $vendor Instance of Vendor Manager Class - * @property WeDevs\Dokan\BackgroundProcess\Manager $bg_process Instance of WeDevs\Dokan\BackgroundProcess\Manager class - * @property WeDevs\Dokan\Withdraw\Manager $withdraw Instance of WeDevs\Dokan\Withdraw\Manager class - * @property WeDevs\Dokan\Frontend\Frontend $frontend_manager Instance of \WeDevs\Dokan\Frontend\Frontend class - * @property WeDevs\Dokan\Registration $registration Instance of WeDevs\Dokan\Registration class - */ -final class WeDevs_Dokan { - - /** - * Plugin version - * - * @var string - */ - public $version = '3.12.6'; - - /** - * Instance of self - * - * @var WeDevs_Dokan - */ - private static $instance = null; - - /** - * Minimum PHP version required - * - * @var string - */ - private $min_php = '7.4'; - - /** - * Holds various class instances - * - * @since 2.6.10 - * - * @var array - */ - private $container = []; - - /** - * Databse version key - * - * @since 3.0.0 - * - * @var string - */ - private $db_version_key = 'dokan_theme_version'; - - /** - * Constructor for the WeDevs_Dokan class - * - * Sets up all the appropriate hooks and actions - * within our plugin. - */ - private function __construct() { - require_once __DIR__ . '/vendor/autoload.php'; - - $this->define_constants(); - - register_activation_hook( __FILE__, [ $this, 'activate' ] ); - register_deactivation_hook( __FILE__, [ $this, 'deactivate' ] ); - - add_action( 'before_woocommerce_init', [ $this, 'declare_woocommerce_feature_compatibility' ] ); - add_action( 'woocommerce_loaded', [ $this, 'init_plugin' ] ); - add_action( 'woocommerce_flush_rewrite_rules', [ $this, 'flush_rewrite_rules' ] ); - - // Register admin notices to container and load notices - $this->container['admin_notices'] = new \WeDevs\Dokan\Admin\Notices\Manager(); - - $this->init_appsero_tracker(); - - add_action( 'plugins_loaded', [ $this, 'woocommerce_not_loaded' ], 11 ); - } - - /** - * Initializes the WeDevs_Dokan() class - * - * Checks for an existing WeDevs_WeDevs_Dokan() instance - * and if it doesn't find one, create it. - */ - public static function init() { - if ( self::$instance === null ) { - self::$instance = new self(); - } - - return self::$instance; - } - - /** - * Magic getter to bypass referencing objects - * - * @since 2.6.10 - * - * @param string $prop - * - * @return object Class Instance - */ - public function __get( $prop ) { - if ( array_key_exists( $prop, $this->container ) ) { - return $this->container[ $prop ]; - } - } - - /** - * Check if the PHP version is supported - * - * @return bool - */ - public function is_supported_php() { - if ( version_compare( PHP_VERSION, $this->min_php, '<=' ) ) { - return false; - } - - return true; - } - - /** - * Get the plugin path. - * - * @return string - */ - public function plugin_path() { - return untrailingslashit( plugin_dir_path( __FILE__ ) ); - } - - /** - * Get the template path. - * - * @return string - */ - public function template_path() { - return apply_filters( 'dokan_template_path', 'dokan/' ); - } - - /** - * Placeholder for activation function - * - * Nothing being called here yet. - */ - public function activate() { - if ( ! $this->has_woocommerce() ) { - set_transient( 'dokan_wc_missing_notice', true ); - } - - if ( ! $this->is_supported_php() ) { - require_once WC_ABSPATH . 'includes/wc-notice-functions.php'; - - /* translators: 1: Required PHP Version 2: Running php version */ - wc_print_notice( sprintf( __( 'The Minimum PHP Version Requirement for Dokan is %1$s. You are Running PHP %2$s', 'dokan-lite' ), $this->min_php, phpversion() ), 'error' ); - exit; - } - - require_once __DIR__ . '/includes/functions.php'; - require_once __DIR__ . '/includes/functions-compatibility.php'; - - $this->container['upgrades'] = new \WeDevs\Dokan\Upgrade\Manager(); - $installer = new \WeDevs\Dokan\Install\Installer(); - $installer->do_install(); - - // rewrite rules during dokan activation - if ( $this->has_woocommerce() ) { - $this->flush_rewrite_rules(); - } - } - - /** - * Flush rewrite rules after dokan is activated or woocommerce is activated - * - * @since 3.2.8 - */ - public function flush_rewrite_rules() { - // fix rewrite rules - if ( ! isset( $this->container['rewrite'] ) ) { - $this->container['rewrite'] = new \WeDevs\Dokan\Rewrites(); - } - $this->container['rewrite']->register_rule(); - flush_rewrite_rules(); - } - - /** - * Placeholder for deactivation function - * - * Nothing being called here yet. - */ - public function deactivate() { - delete_transient( 'dokan_wc_missing_notice', true ); - } - - /** - * Initialize plugin for localization - * - * @uses load_plugin_textdomain() - */ - public function localization_setup() { - load_plugin_textdomain( 'dokan-lite', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' ); - } - - /** - * Define all constants - * - * @return void - */ - public function define_constants() { - $this->define( 'DOKAN_PLUGIN_VERSION', $this->version ); - $this->define( 'DOKAN_FILE', __FILE__ ); - $this->define( 'DOKAN_DIR', __DIR__ ); - $this->define( 'DOKAN_INC_DIR', __DIR__ . '/includes' ); - $this->define( 'DOKAN_LIB_DIR', __DIR__ . '/lib' ); - $this->define( 'DOKAN_PLUGIN_ASSEST', plugins_url( 'assets', __FILE__ ) ); - - // give a way to turn off loading styles and scripts from parent theme - $this->define( 'DOKAN_LOAD_STYLE', true ); - $this->define( 'DOKAN_LOAD_SCRIPTS', true ); - } - - /** - * Define constant if not already defined - * - * @since 2.9.16 - * - * @param string $name - * @param string|bool $value - * - * @return void - */ - private function define( $name, $value ) { - if ( ! defined( $name ) ) { - define( $name, $value ); - } - } +require_once __DIR__ . '/vendor/autoload.php'; +// Load files for loading the WeDevs_Dokan class. +require_once __DIR__ . '/dokan-class.php'; - /** - * Add High Performance Order Storage Support - * - * @since 3.8.0 - * - * @return void - */ - public function declare_woocommerce_feature_compatibility() { - if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) { - \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); - \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true ); - } - } +// Define constant for the Plugin file. +defined( 'DOKAN_FILE' ) || define( 'DOKAN_FILE', __FILE__ ); - /** - * Load the plugin after WP User Frontend is loaded - * - * @return void - */ - public function init_plugin() { - $this->includes(); - $this->init_hooks(); +// Use the necessary namespace. +use WeDevs\Dokan\DependencyManagement\Container; - do_action( 'dokan_loaded' ); - } +// Declare the $dokan_container as global to access from the inside of the function. +global $dokan_container; - /** - * Initialize the actions - * - * @return void - */ - public function init_hooks() { - // Localize our plugin - add_action( 'init', [ $this, 'localization_setup' ] ); +// Instantiate the container. +$dokan_container = new Container(); - // initialize the classes - add_action( 'init', [ $this, 'init_classes' ], 4 ); - add_action( 'init', [ $this, 'wpdb_table_shortcuts' ], 1 ); +// Register the service providers. +$dokan_container->addServiceProvider( new \WeDevs\Dokan\DependencyManagement\Providers\ServiceProvider() ); - add_action( 'plugins_loaded', [ $this, 'after_plugins_loaded' ] ); - - add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), [ $this, 'plugin_action_links' ] ); - add_action( 'in_plugin_update_message-dokan-lite/dokan.php', [ \WeDevs\Dokan\Install\Installer::class, 'in_plugin_update_message' ] ); - - add_action( 'widgets_init', [ $this, 'register_widgets' ] ); - } - - /** - * Include all the required files - * - * @return void - */ - public function includes() { - require_once DOKAN_DIR . '/deprecated/deprecated-functions.php'; - require_once DOKAN_DIR . '/deprecated/deprecated-hooks.php'; - require_once DOKAN_INC_DIR . '/functions.php'; - - if ( ! function_exists( 'dokan_pro' ) ) { - require_once DOKAN_INC_DIR . '/reports.php'; - } - - require_once DOKAN_INC_DIR . '/Order/functions.php'; - require_once DOKAN_INC_DIR . '/Product/functions.php'; - require_once DOKAN_INC_DIR . '/Withdraw/functions.php'; - require_once DOKAN_INC_DIR . '/functions-compatibility.php'; - require_once DOKAN_INC_DIR . '/wc-functions.php'; - - require_once DOKAN_INC_DIR . '/wc-template.php'; - require_once DOKAN_DIR . '/deprecated/deprecated-classes.php'; - - if ( is_admin() ) { - require_once DOKAN_INC_DIR . '/Admin/functions.php'; - } else { - require_once DOKAN_INC_DIR . '/template-tags.php'; - } - - require_once DOKAN_INC_DIR . '/store-functions.php'; - } - - /** - * Init all the classes - * - * @return void - */ - public function init_classes() { - new \WeDevs\Dokan\Withdraw\Hooks(); - new \WeDevs\Dokan\Product\Hooks(); - new \WeDevs\Dokan\ProductCategory\Hooks(); - new \WeDevs\Dokan\Vendor\Hooks(); - new \WeDevs\Dokan\Upgrade\Hooks(); - new \WeDevs\Dokan\Vendor\UserSwitch(); - new \WeDevs\Dokan\CacheInvalidate(); - new \WeDevs\Dokan\Shipping\Hooks(); - - if ( is_admin() ) { - new \WeDevs\Dokan\Admin\Hooks(); - new \WeDevs\Dokan\Admin\Menu(); - new \WeDevs\Dokan\Admin\AdminBar(); - new \WeDevs\Dokan\Admin\Pointers(); - new \WeDevs\Dokan\Admin\Settings(); - new \WeDevs\Dokan\Admin\UserProfile(); - new \WeDevs\Dokan\Admin\SetupWizard(); - } else { - new \WeDevs\Dokan\Vendor\StoreListsFilter(); - new \WeDevs\Dokan\ThemeSupport\Manager(); - } - - $this->container['product_block'] = new \WeDevs\Dokan\Blocks\ProductBlock(); - $this->container['pageview'] = new \WeDevs\Dokan\PageViews(); - $this->container['seller_wizard'] = new \WeDevs\Dokan\Vendor\SetupWizard(); - $this->container['core'] = new \WeDevs\Dokan\Core(); - $this->container['scripts'] = new \WeDevs\Dokan\Assets(); - $this->container['email'] = new \WeDevs\Dokan\Emails\Manager(); - $this->container['vendor'] = new \WeDevs\Dokan\Vendor\Manager(); - $this->container['product'] = new \WeDevs\Dokan\Product\Manager(); - $this->container['shortcodes'] = new \WeDevs\Dokan\Shortcodes\Shortcodes(); - $this->container['registration'] = new \WeDevs\Dokan\Registration(); - $this->container['order'] = new \WeDevs\Dokan\Order\Manager(); - $this->container['order_controller'] = new \WeDevs\Dokan\Order\Controller(); - $this->container['api'] = new \WeDevs\Dokan\REST\Manager(); - $this->container['withdraw'] = new \WeDevs\Dokan\Withdraw\Manager(); - $this->container['dashboard'] = new \WeDevs\Dokan\Dashboard\Manager(); - $this->container['commission'] = new \WeDevs\Dokan\Commission(); - $this->container['customizer'] = new \WeDevs\Dokan\Customizer(); - $this->container['upgrades'] = new \WeDevs\Dokan\Upgrade\Manager(); - $this->container['product_sections'] = new \WeDevs\Dokan\ProductSections\Manager(); - $this->container['reverse_withdrawal'] = new \WeDevs\Dokan\ReverseWithdrawal\ReverseWithdrawal(); - $this->container['dummy_data_importer'] = new \WeDevs\Dokan\DummyData\Importer(); - $this->container['catalog_mode'] = new \WeDevs\Dokan\CatalogMode\Controller(); - $this->container['bg_process'] = new \WeDevs\Dokan\BackgroundProcess\Manager(); - $this->container['frontend_manager'] = new \WeDevs\Dokan\Frontend\Frontend(); - - //fix rewrite rules - if ( ! isset( $this->container['rewrite'] ) ) { - $this->container['rewrite'] = new \WeDevs\Dokan\Rewrites(); - } - - $this->container = apply_filters( 'dokan_get_class_container', $this->container ); - - if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { - new \WeDevs\Dokan\Ajax(); - } - - new \WeDevs\Dokan\Privacy(); - } - - /** - * Load table prefix for withdraw and orders table - * - * @since 1.0 - * - * @return void - */ - public function wpdb_table_shortcuts() { - global $wpdb; - - $wpdb->dokan_withdraw = $wpdb->prefix . 'dokan_withdraw'; - $wpdb->dokan_orders = $wpdb->prefix . 'dokan_orders'; - $wpdb->dokan_announcement = $wpdb->prefix . 'dokan_announcement'; - $wpdb->dokan_refund = $wpdb->prefix . 'dokan_refund'; - $wpdb->dokan_vendor_balance = $wpdb->prefix . 'dokan_vendor_balance'; - } - - /** - * Executed after all plugins are loaded - * - * At this point Dokan Pro is loaded - * - * @since 2.8.7 - * - * @return void - */ - public function after_plugins_loaded() { - // Initiate background processes - $processes = get_option( 'dokan_background_processes', [] ); - - if ( ! empty( $processes ) ) { - $update = false; - foreach ( $processes as $processor => $file ) { - if ( file_exists( $file ) ) { - include_once $file; - new $processor(); - } else { - $update = true; - unset( $processes[ $processor ] ); - } - } - if ( $update ) { - update_option( 'dokan_background_processes', $processes ); - } - } - } - - /** - * Register widgets - * - * @since 2.8 - * - * @return void - */ - public function register_widgets() { - $this->container['widgets'] = new \WeDevs\Dokan\Widgets\Manager(); - } - - /** - * Returns if the plugin is in PRO version - * - * @since 2.4 - * - * @return bool - */ - public function is_pro_exists() { - return apply_filters( 'dokan_is_pro_exists', false ); - } - - /** - * Plugin action links - * - * @param array $links - * - * @since 2.4 - * - * @return array - */ - public function plugin_action_links( $links ) { - if ( ! $this->is_pro_exists() ) { - $links[] = '' . __( 'Get Pro', 'dokan-lite' ) . ''; - } - - $links[] = '' . __( 'Settings', 'dokan-lite' ) . ''; - $links[] = '' . __( 'Documentation', 'dokan-lite' ) . ''; - - return $links; - } - - /** - * Initialize Appsero Tracker - * - * @return void - */ - public function init_appsero_tracker() { - $this->container['tracker'] = new \WeDevs\Dokan\Tracker(); - } - - /** - * Check whether woocommerce is installed and active - * - * @since 2.9.16 - * - * @return bool - */ - public function has_woocommerce() { - return class_exists( 'WooCommerce' ); - } - - /** - * Check whether woocommerce is installed - * - * @since 3.2.8 - * - * @return bool - */ - public function is_woocommerce_installed() { - return in_array( 'woocommerce/woocommerce.php', array_keys( get_plugins() ), true ); - } - - /** - * Handles scenerios when WooCommerce is not active - * - * @since 2.9.27 - * - * @return void - */ - public function woocommerce_not_loaded() { - if ( did_action( 'woocommerce_loaded' ) || ! is_admin() ) { - return; - } - - require_once DOKAN_INC_DIR . '/functions.php'; - - if ( get_transient( '_dokan_setup_page_redirect' ) ) { - dokan_redirect_to_admin_setup_wizard(); - } - - new \WeDevs\Dokan\Admin\SetupWizardNoWC(); - } +/** + * Get the container. + * + * @return Container The global container instance. + */ +function dokan_get_container(): Container { + global $dokan_container; - /** - * Get Dokan db version key - * - * @since 3.0.0 - * - * @return string - */ - public function get_db_version_key() { - return $this->db_version_key; - } + return $dokan_container; } /** - * Load Dokan Plugin when all plugins loaded + * Load Dokan Plugin when all plugins loaded. * - * @return WeDevs_Dokan + * @return WeDevs_Dokan The singleton instance of WeDevs_Dokan. */ -function dokan() { // phpcs:ignore +function dokan() { return WeDevs_Dokan::init(); } -// Lets Go.... +// Let's go... dokan(); diff --git a/includes/Contracts/Hookable.php b/includes/Contracts/Hookable.php new file mode 100644 index 0000000000..9dacefeb32 --- /dev/null +++ b/includes/Contracts/Hookable.php @@ -0,0 +1,19 @@ +services as $class ) { + $implements_more = class_implements( $class ); + if ( $implements_more ) { + $implements = array_merge( $implements, $implements_more ); + } + } + + $implements = array_unique( $implements ); + + return array_key_exists( $alias, $implements ); + } + + /** + * Register a class in the container and add tags for all the interfaces it implements. + * + * This also updates the `$this->provides` property with the interfaces provided by the class, and ensures + * that the property doesn't contain duplicates. + * + * @param string $id Entry ID (typically a class or interface name). + * @param mixed|null $concrete Concrete entity to register under that ID, null for automatic creation. + * @param bool|null $shared Whether to register the class as shared (`get` always returns the same instance) + * or not. + * + * @return DefinitionInterface + */ + protected function add_with_implements_tags( string $id, $concrete = null, bool $shared = null ): DefinitionInterface { + $definition = $this->getContainer()->add( $id, $concrete, $shared ); + + foreach ( class_implements( $id ) as $interface ) { + $definition->addTag( $interface ); + } + + return $definition; + } + + /** + * Register a shared class in the container and add tags for all the interfaces it implements. + * + * @param string $id Entry ID (typically a class or interface name). + * @param mixed|null $concrete Concrete entity to register under that ID, null for automatic creation. + * + * @return DefinitionInterface + */ + protected function share_with_implements_tags( string $id, $concrete = null ): DefinitionInterface { + return $this->add_with_implements_tags( $id, $concrete, true ); + } +} diff --git a/includes/DependencyManagement/BootableServiceProvider.php b/includes/DependencyManagement/BootableServiceProvider.php new file mode 100644 index 0000000000..b07bfadd98 --- /dev/null +++ b/includes/DependencyManagement/BootableServiceProvider.php @@ -0,0 +1,19 @@ +invokeInit( $instance ); + return $instance; + } + + /** + * Invoke methods on resolved instance, including 'init'. + * + * @param object $instance The concrete to invoke methods on. + * + * @return object + */ + protected function invokeMethods( $instance ): object { + $this->invokeInit( $instance ); + parent::invokeMethods( $instance ); + return $instance; + } + + /** + * Invoke the 'init' method on a resolved object. + * + * Constructor injection causes backwards compatibility problems + * so we will rely on method injection via an internal method. + * + * @param object $instance The resolved object. + * @return void + */ + private function invokeInit( $instance ) { + $resolved = $this->resolveArguments( $this->arguments ); + + if ( method_exists( $instance, static::INJECTION_METHOD ) ) { + call_user_func_array( array( $instance, static::INJECTION_METHOD ), $resolved ); + } + } + + /** + * Forget the cached resolved object, so the next time it's requested + * it will be resolved again. + */ + public function forgetResolved() { + $this->resolved = null; + } +} diff --git a/includes/DependencyManagement/Providers/AdminServiceProvider.php b/includes/DependencyManagement/Providers/AdminServiceProvider.php new file mode 100644 index 0000000000..34c48ab984 --- /dev/null +++ b/includes/DependencyManagement/Providers/AdminServiceProvider.php @@ -0,0 +1,58 @@ +services, true ); + } + + /** + * Register the classes. + */ + public function register(): void { + $this->getContainer() + ->addShared( \WeDevs\Dokan\Admin\Hooks::class, \WeDevs\Dokan\Admin\Hooks::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\Admin\Menu::class, \WeDevs\Dokan\Admin\Menu::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\Admin\AdminBar::class, \WeDevs\Dokan\Admin\AdminBar::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\Admin\Pointers::class, \WeDevs\Dokan\Admin\Pointers::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\Admin\Settings::class, \WeDevs\Dokan\Admin\Settings::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\Admin\UserProfile::class, \WeDevs\Dokan\Admin\UserProfile::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\Admin\SetupWizard::class, \WeDevs\Dokan\Admin\SetupWizard::class ) + ->addTag( self::TAG ); + } +} diff --git a/includes/DependencyManagement/Providers/AjaxServiceProvider.php b/includes/DependencyManagement/Providers/AjaxServiceProvider.php new file mode 100644 index 0000000000..e76b59337f --- /dev/null +++ b/includes/DependencyManagement/Providers/AjaxServiceProvider.php @@ -0,0 +1,34 @@ +services, true ); + } + + /** + * Register the classes. + */ + public function register(): void { + $this->getContainer() + ->addShared( \WeDevs\Dokan\Ajax::class, \WeDevs\Dokan\Ajax::class ) + ->addTag( self::TAG ); + } +} diff --git a/includes/DependencyManagement/Providers/CommonServiceProvider.php b/includes/DependencyManagement/Providers/CommonServiceProvider.php new file mode 100644 index 0000000000..4d59f0ca24 --- /dev/null +++ b/includes/DependencyManagement/Providers/CommonServiceProvider.php @@ -0,0 +1,66 @@ +services, true ); + } + + /** + * Register the classes. + */ + public function register(): void { + $this->getContainer() + ->addShared( \WeDevs\Dokan\Withdraw\Hooks::class, \WeDevs\Dokan\Withdraw\Hooks::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\Product\Hooks::class, \WeDevs\Dokan\Product\Hooks::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\ProductCategory\Hooks::class, \WeDevs\Dokan\ProductCategory\Hooks::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\Upgrade\Hooks::class, \WeDevs\Dokan\Upgrade\Hooks::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\Vendor\Hooks::class, \WeDevs\Dokan\Vendor\Hooks::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\Vendor\UserSwitch::class, \WeDevs\Dokan\Vendor\UserSwitch::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\CacheInvalidate::class, \WeDevs\Dokan\CacheInvalidate::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\Shipping\Hooks::class, \WeDevs\Dokan\Shipping\Hooks::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\Privacy::class, \WeDevs\Dokan\Privacy::class ) + ->addTag( self::TAG ); + } +} diff --git a/includes/DependencyManagement/Providers/FrontendServiceProvider.php b/includes/DependencyManagement/Providers/FrontendServiceProvider.php new file mode 100644 index 0000000000..5eb6681f15 --- /dev/null +++ b/includes/DependencyManagement/Providers/FrontendServiceProvider.php @@ -0,0 +1,38 @@ +services, true ); + } + + /** + * Register the classes. + */ + public function register(): void { + $this->getContainer() + ->addShared( \WeDevs\Dokan\Vendor\StoreListsFilter::class, \WeDevs\Dokan\Vendor\StoreListsFilter::class ) + ->addTag( self::TAG ); + + $this->getContainer() + ->addShared( \WeDevs\Dokan\ThemeSupport\Manager::class, \WeDevs\Dokan\ThemeSupport\Manager::class ) + ->addTag( self::TAG ); + } +} diff --git a/includes/DependencyManagement/Providers/ServiceProvider.php b/includes/DependencyManagement/Providers/ServiceProvider.php new file mode 100644 index 0000000000..d7aa3f0b9d --- /dev/null +++ b/includes/DependencyManagement/Providers/ServiceProvider.php @@ -0,0 +1,77 @@ + \WeDevs\Dokan\Blocks\ProductBlock::class, + 'pageview' => \WeDevs\Dokan\PageViews::class, + 'seller_wizard' => \WeDevs\Dokan\Vendor\SetupWizard::class, + 'core' => \WeDevs\Dokan\Core::class, + 'scripts' => \WeDevs\Dokan\Assets::class, + 'email' => \WeDevs\Dokan\Emails\Manager::class, + 'vendor' => \WeDevs\Dokan\Vendor\Manager::class, + 'product' => \WeDevs\Dokan\Product\Manager::class, + 'shortcodes' => \WeDevs\Dokan\Shortcodes\Shortcodes::class, + 'registration' => \WeDevs\Dokan\Registration::class, + 'order' => \WeDevs\Dokan\Order\Manager::class, + 'order_controller' => \WeDevs\Dokan\Order\Controller::class, + 'api' => \WeDevs\Dokan\REST\Manager::class, + 'withdraw' => \WeDevs\Dokan\Withdraw\Manager::class, + 'dashboard' => \WeDevs\Dokan\Dashboard\Manager::class, + 'commission' => \WeDevs\Dokan\Commission::class, + 'customizer' => \WeDevs\Dokan\Customizer::class, + 'upgrades' => \WeDevs\Dokan\Upgrade\Manager::class, + 'product_sections' => \WeDevs\Dokan\ProductSections\Manager::class, + 'reverse_withdrawal' => \WeDevs\Dokan\ReverseWithdrawal\ReverseWithdrawal::class, + 'dummy_data_importer' => \WeDevs\Dokan\DummyData\Importer::class, + 'catalog_mode' => \WeDevs\Dokan\CatalogMode\Controller::class, + 'bg_process' => \WeDevs\Dokan\BackgroundProcess\Manager::class, + 'frontend_manager' => \WeDevs\Dokan\Frontend\Frontend::class, + 'rewrite' => \WeDevs\Dokan\Rewrites::class, + 'widgets' => \WeDevs\Dokan\Widgets\Manager::class, + 'admin_notices' => \WeDevs\Dokan\Admin\Notices\Manager::class, + 'tracker' => \WeDevs\Dokan\Tracker::class, + ]; + + /** + * @inheritDoc + * + * @return void + */ + public function boot(): void { + $this->getContainer()->addServiceProvider( new AdminServiceProvider() ); + $this->getContainer()->addServiceProvider( new CommonServiceProvider() ); + $this->getContainer()->addServiceProvider( new FrontendServiceProvider() ); + $this->getContainer()->addServiceProvider( new AjaxServiceProvider() ); + } + + /** + * {@inheritDoc} + * + * Check if the service provider can provide the given service alias. + * + * @param string $alias The service alias to check. + * @return bool True if the service provider can provide the service, false otherwise. + */ + public function provides( string $alias ): bool { + if ( isset( $this->services[ $alias ] ) ) { + return true; + } + + return parent::provides( $alias ); + } + + /** + * Register the classes. + */ + public function register(): void { + foreach ( $this->services as $key => $class_name ) { + $this->getContainer()->addShared( $key, $class_name )->addTag( self::TAG ); + } + } +} diff --git a/lib/packages/League/Container/Argument/ArgumentInterface.php b/lib/packages/League/Container/Argument/ArgumentInterface.php new file mode 100644 index 0000000000..bc3c864e35 --- /dev/null +++ b/lib/packages/League/Container/Argument/ArgumentInterface.php @@ -0,0 +1,13 @@ +getContainer(); + } catch (ContainerException $e) { + $container = ($this instanceof ReflectionContainer) ? $this : null; + } + + foreach ($arguments as &$arg) { + // if we have a literal, we don't want to do anything more with it + if ($arg instanceof LiteralArgumentInterface) { + $arg = $arg->getValue(); + continue; + } + + if ($arg instanceof ArgumentInterface) { + $argValue = $arg->getValue(); + } else { + $argValue = $arg; + } + + if (!is_string($argValue)) { + continue; + } + + // resolve the argument from the container, if it happens to be another + // argument wrapper, use that value + if ($container instanceof ContainerInterface && $container->has($argValue)) { + try { + $arg = $container->get($argValue); + + if ($arg instanceof ArgumentInterface) { + $arg = $arg->getValue(); + } + + continue; + } catch (NotFoundException $e) { + } + } + + // if we have a default value, we use that, no more resolution as + // we expect a default/optional argument value to be literal + if ($arg instanceof DefaultValueInterface) { + $arg = $arg->getDefaultValue(); + } + } + + return $arguments; + } + + public function reflectArguments(ReflectionFunctionAbstract $method, array $args = []): array + { + $params = $method->getParameters(); + $arguments = []; + + foreach ($params as $param) { + $name = $param->getName(); + + // if we've been given a value for the argument, treat as literal + if (array_key_exists($name, $args)) { + $arguments[] = new LiteralArgument($args[$name]); + continue; + } + + $type = $param->getType(); + + if ($type instanceof ReflectionNamedType) { + // in PHP 8, nullable arguments have "?" prefix + $typeHint = ltrim($type->getName(), '?'); + + if ($param->isDefaultValueAvailable()) { + $arguments[] = new DefaultValueArgument($typeHint, $param->getDefaultValue()); + continue; + } + + $arguments[] = new ResolvableArgument($typeHint); + continue; + } + + if ($param->isDefaultValueAvailable()) { + $arguments[] = new LiteralArgument($param->getDefaultValue()); + continue; + } + + throw new NotFoundException(sprintf( + 'Unable to resolve a value for parameter (%s) in the function/method (%s)', + $name, + $method->getName() + )); + } + + return $this->resolveArguments($arguments); + } + + abstract public function getContainer(): DefinitionContainerInterface; +} diff --git a/lib/packages/League/Container/Argument/DefaultValueArgument.php b/lib/packages/League/Container/Argument/DefaultValueArgument.php new file mode 100644 index 0000000000..cf3c436e43 --- /dev/null +++ b/lib/packages/League/Container/Argument/DefaultValueArgument.php @@ -0,0 +1,24 @@ +defaultValue = $defaultValue; + parent::__construct($value); + } + + /** + * @return mixed|null + */ + public function getDefaultValue() + { + return $this->defaultValue; + } +} diff --git a/lib/packages/League/Container/Argument/DefaultValueInterface.php b/lib/packages/League/Container/Argument/DefaultValueInterface.php new file mode 100644 index 0000000000..879fe903a4 --- /dev/null +++ b/lib/packages/League/Container/Argument/DefaultValueInterface.php @@ -0,0 +1,13 @@ +value = $value; + } else { + throw new InvalidArgumentException('Incorrect type for value.'); + } + } + + /** + * {@inheritdoc} + */ + public function getValue() + { + return $this->value; + } +} diff --git a/lib/packages/League/Container/Argument/LiteralArgumentInterface.php b/lib/packages/League/Container/Argument/LiteralArgumentInterface.php new file mode 100644 index 0000000000..4cb06ff980 --- /dev/null +++ b/lib/packages/League/Container/Argument/LiteralArgumentInterface.php @@ -0,0 +1,9 @@ +value = $value; + } + + public function getValue(): string + { + return $this->value; + } +} diff --git a/lib/packages/League/Container/Argument/ResolvableArgumentInterface.php b/lib/packages/League/Container/Argument/ResolvableArgumentInterface.php new file mode 100644 index 0000000000..e7136e6c4b --- /dev/null +++ b/lib/packages/League/Container/Argument/ResolvableArgumentInterface.php @@ -0,0 +1,10 @@ +definitions = $definitions ?? new DefinitionAggregate(); + $this->providers = $providers ?? new ServiceProviderAggregate(); + $this->inflectors = $inflectors ?? new InflectorAggregate(); + + if ($this->definitions instanceof ContainerAwareInterface) { + $this->definitions->setContainer($this); + } + + if ($this->providers instanceof ContainerAwareInterface) { + $this->providers->setContainer($this); + } + + if ($this->inflectors instanceof ContainerAwareInterface) { + $this->inflectors->setContainer($this); + } + } + + public function add(string $id, $concrete = null): DefinitionInterface + { + $concrete = $concrete ?? $id; + + if (true === $this->defaultToShared) { + return $this->addShared($id, $concrete); + } + + return $this->definitions->add($id, $concrete); + } + + public function addShared(string $id, $concrete = null): DefinitionInterface + { + $concrete = $concrete ?? $id; + return $this->definitions->addShared($id, $concrete); + } + + public function defaultToShared(bool $shared = true): ContainerInterface + { + $this->defaultToShared = $shared; + return $this; + } + + public function extend(string $id): DefinitionInterface + { + if ($this->providers->provides($id)) { + $this->providers->register($id); + } + + if ($this->definitions->has($id)) { + return $this->definitions->getDefinition($id); + } + + throw new NotFoundException(sprintf( + 'Unable to extend alias (%s) as it is not being managed as a definition', + $id + )); + } + + public function addServiceProvider(ServiceProviderInterface $provider): DefinitionContainerInterface + { + $this->providers->add($provider); + return $this; + } + + /** + * @template RequestedType + * + * @param class-string|string $id + * + * @return RequestedType|mixed + */ + public function get($id) + { + return $this->resolve($id); + } + + /** + * @template RequestedType + * + * @param class-string|string $id + * + * @return RequestedType|mixed + */ + public function getNew($id) + { + return $this->resolve($id, true); + } + + public function has($id): bool + { + if ($this->definitions->has($id)) { + return true; + } + + if ($this->definitions->hasTag($id)) { + return true; + } + + if ($this->providers->provides($id)) { + return true; + } + + foreach ($this->delegates as $delegate) { + if ($delegate->has($id)) { + return true; + } + } + + return false; + } + + public function inflector(string $type, callable $callback = null): InflectorInterface + { + return $this->inflectors->add($type, $callback); + } + + public function delegate(ContainerInterface $container): self + { + $this->delegates[] = $container; + + if ($container instanceof ContainerAwareInterface) { + $container->setContainer($this); + } + + return $this; + } + + protected function resolve($id, bool $new = false) + { + if ($this->definitions->has($id)) { + $resolved = (true === $new) ? $this->definitions->resolveNew($id) : $this->definitions->resolve($id); + return $this->inflectors->inflect($resolved); + } + + if ($this->definitions->hasTag($id)) { + $arrayOf = (true === $new) + ? $this->definitions->resolveTaggedNew($id) + : $this->definitions->resolveTagged($id); + + array_walk($arrayOf, function (&$resolved) { + $resolved = $this->inflectors->inflect($resolved); + }); + + return $arrayOf; + } + + if ($this->providers->provides($id)) { + $this->providers->register($id); + + if (!$this->definitions->has($id) && !$this->definitions->hasTag($id)) { + throw new ContainerException(sprintf('Service provider lied about providing (%s) service', $id)); + } + + return $this->resolve($id, $new); + } + + foreach ($this->delegates as $delegate) { + if ($delegate->has($id)) { + $resolved = $delegate->get($id); + return $this->inflectors->inflect($resolved); + } + } + + throw new NotFoundException(sprintf('Alias (%s) is not being managed by the container or delegates', $id)); + } +} diff --git a/lib/packages/League/Container/ContainerAwareInterface.php b/lib/packages/League/Container/ContainerAwareInterface.php new file mode 100644 index 0000000000..494ff2b244 --- /dev/null +++ b/lib/packages/League/Container/ContainerAwareInterface.php @@ -0,0 +1,11 @@ +container = $container; + + if ($this instanceof ContainerAwareInterface) { + return $this; + } + + throw new BadMethodCallException(sprintf( + 'Attempt to use (%s) while not implementing (%s)', + ContainerAwareTrait::class, + ContainerAwareInterface::class + )); + } + + public function getContainer(): DefinitionContainerInterface + { + if ($this->container instanceof DefinitionContainerInterface) { + return $this->container; + } + + throw new ContainerException('No container implementation has been set.'); + } +} diff --git a/lib/packages/League/Container/Definition/Definition.php b/lib/packages/League/Container/Definition/Definition.php new file mode 100644 index 0000000000..df0fb488e6 --- /dev/null +++ b/lib/packages/League/Container/Definition/Definition.php @@ -0,0 +1,238 @@ +alias = $id; + $this->concrete = $concrete; + } + + public function addTag(string $tag): DefinitionInterface + { + $this->tags[$tag] = true; + return $this; + } + + public function hasTag(string $tag): bool + { + return isset($this->tags[$tag]); + } + + public function setAlias(string $id): DefinitionInterface + { + $this->alias = $id; + return $this; + } + + public function getAlias(): string + { + return $this->alias; + } + + public function setShared(bool $shared = true): DefinitionInterface + { + $this->shared = $shared; + return $this; + } + + public function isShared(): bool + { + return $this->shared; + } + + public function getConcrete() + { + return $this->concrete; + } + + public function setConcrete($concrete): DefinitionInterface + { + $this->concrete = $concrete; + $this->resolved = null; + return $this; + } + + public function addArgument($arg): DefinitionInterface + { + $this->arguments[] = $arg; + return $this; + } + + public function addArguments(array $args): DefinitionInterface + { + foreach ($args as $arg) { + $this->addArgument($arg); + } + + return $this; + } + + public function addMethodCall(string $method, array $args = []): DefinitionInterface + { + $this->methods[] = [ + 'method' => $method, + 'arguments' => $args + ]; + + return $this; + } + + public function addMethodCalls(array $methods = []): DefinitionInterface + { + foreach ($methods as $method => $args) { + $this->addMethodCall($method, $args); + } + + return $this; + } + + public function resolve() + { + if (null !== $this->resolved && $this->isShared()) { + return $this->resolved; + } + + return $this->resolveNew(); + } + + public function resolveNew() + { + $concrete = $this->concrete; + + if (is_callable($concrete)) { + $concrete = $this->resolveCallable($concrete); + } + + if ($concrete instanceof LiteralArgumentInterface) { + $this->resolved = $concrete->getValue(); + return $concrete->getValue(); + } + + if ($concrete instanceof ArgumentInterface) { + $concrete = $concrete->getValue(); + } + + if (is_string($concrete) && class_exists($concrete)) { + $concrete = $this->resolveClass($concrete); + } + + if (is_object($concrete)) { + $concrete = $this->invokeMethods($concrete); + } + + try { + $container = $this->getContainer(); + } catch (ContainerException $e) { + $container = null; + } + + // stop recursive resolving + if (is_string($concrete) && in_array($concrete, $this->recursiveCheck)) { + $this->resolved = $concrete; + return $concrete; + } + + // if we still have a string, try to pull it from the container + // this allows for `alias -> alias -> ... -> concrete + if (is_string($concrete) && $container instanceof ContainerInterface && $container->has($concrete)) { + $this->recursiveCheck[] = $concrete; + $concrete = $container->get($concrete); + } + + $this->resolved = $concrete; + return $concrete; + } + + /** + * @param callable $concrete + * @return mixed + */ + protected function resolveCallable(callable $concrete) + { + $resolved = $this->resolveArguments($this->arguments); + return call_user_func_array($concrete, $resolved); + } + + protected function resolveClass(string $concrete): object + { + $resolved = $this->resolveArguments($this->arguments); + $reflection = new ReflectionClass($concrete); + return $reflection->newInstanceArgs($resolved); + } + + protected function invokeMethods(object $instance): object + { + foreach ($this->methods as $method) { + $args = $this->resolveArguments($method['arguments']); + $callable = [$instance, $method['method']]; + call_user_func_array($callable, $args); + } + + return $instance; + } +} diff --git a/lib/packages/League/Container/Definition/DefinitionAggregate.php b/lib/packages/League/Container/Definition/DefinitionAggregate.php new file mode 100644 index 0000000000..96976f6990 --- /dev/null +++ b/lib/packages/League/Container/Definition/DefinitionAggregate.php @@ -0,0 +1,117 @@ +definitions = array_filter($definitions, static function ($definition) { + return ($definition instanceof DefinitionInterface); + }); + } + + public function add(string $id, $definition): DefinitionInterface + { + if (false === ($definition instanceof DefinitionInterface)) { + $definition = new Definition($id, $definition); + } + + $this->definitions[] = $definition->setAlias($id); + + return $definition; + } + + public function addShared(string $id, $definition): DefinitionInterface + { + $definition = $this->add($id, $definition); + return $definition->setShared(true); + } + + public function has(string $id): bool + { + foreach ($this->getIterator() as $definition) { + if ($id === $definition->getAlias()) { + return true; + } + } + + return false; + } + + public function hasTag(string $tag): bool + { + foreach ($this->getIterator() as $definition) { + if ($definition->hasTag($tag)) { + return true; + } + } + + return false; + } + + public function getDefinition(string $id): DefinitionInterface + { + foreach ($this->getIterator() as $definition) { + if ($id === $definition->getAlias()) { + return $definition->setContainer($this->getContainer()); + } + } + + throw new NotFoundException(sprintf('Alias (%s) is not being handled as a definition.', $id)); + } + + public function resolve(string $id) + { + return $this->getDefinition($id)->resolve(); + } + + public function resolveNew(string $id) + { + return $this->getDefinition($id)->resolveNew(); + } + + public function resolveTagged(string $tag): array + { + $arrayOf = []; + + foreach ($this->getIterator() as $definition) { + if ($definition->hasTag($tag)) { + $arrayOf[] = $definition->setContainer($this->getContainer())->resolve(); + } + } + + return $arrayOf; + } + + public function resolveTaggedNew(string $tag): array + { + $arrayOf = []; + + foreach ($this->getIterator() as $definition) { + if ($definition->hasTag($tag)) { + $arrayOf[] = $definition->setContainer($this->getContainer())->resolveNew(); + } + } + + return $arrayOf; + } + + public function getIterator(): Generator + { + yield from $this->definitions; + } +} diff --git a/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php b/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php new file mode 100644 index 0000000000..13eca48995 --- /dev/null +++ b/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php @@ -0,0 +1,21 @@ +type = $type; + $this->callback = $callback; + } + + public function getType(): string + { + return $this->type; + } + + public function invokeMethod(string $name, array $args): InflectorInterface + { + $this->methods[$name] = $args; + return $this; + } + + public function invokeMethods(array $methods): InflectorInterface + { + foreach ($methods as $name => $args) { + $this->invokeMethod($name, $args); + } + + return $this; + } + + public function setProperty(string $property, $value): InflectorInterface + { + $this->properties[$property] = $this->resolveArguments([$value])[0]; + return $this; + } + + public function setProperties(array $properties): InflectorInterface + { + foreach ($properties as $property => $value) { + $this->setProperty($property, $value); + } + + return $this; + } + + public function inflect(object $object): void + { + $properties = $this->resolveArguments(array_values($this->properties)); + $properties = array_combine(array_keys($this->properties), $properties); + + // array_combine() can technically return false + foreach ($properties ?: [] as $property => $value) { + $object->{$property} = $value; + } + + foreach ($this->methods as $method => $args) { + $args = $this->resolveArguments($args); + $callable = [$object, $method]; + call_user_func_array($callable, $args); + } + + if ($this->callback !== null) { + call_user_func($this->callback, $object); + } + } +} diff --git a/lib/packages/League/Container/Inflector/InflectorAggregate.php b/lib/packages/League/Container/Inflector/InflectorAggregate.php new file mode 100644 index 0000000000..2a20fe5707 --- /dev/null +++ b/lib/packages/League/Container/Inflector/InflectorAggregate.php @@ -0,0 +1,44 @@ +inflectors[] = $inflector; + return $inflector; + } + + public function inflect($object) + { + foreach ($this->getIterator() as $inflector) { + $type = $inflector->getType(); + + if ($object instanceof $type) { + $inflector->setContainer($this->getContainer()); + $inflector->inflect($object); + } + } + + return $object; + } + + public function getIterator(): Generator + { + yield from $this->inflectors; + } +} diff --git a/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php b/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php new file mode 100644 index 0000000000..9395850bf0 --- /dev/null +++ b/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php @@ -0,0 +1,14 @@ +cacheResolutions = $cacheResolutions; + } + + public function get($id, array $args = []) + { + if ($this->cacheResolutions === true && array_key_exists($id, $this->cache)) { + return $this->cache[$id]; + } + + if (!$this->has($id)) { + throw new NotFoundException( + sprintf('Alias (%s) is not an existing class and therefore cannot be resolved', $id) + ); + } + + $reflector = new ReflectionClass($id); + $construct = $reflector->getConstructor(); + + if ($construct && !$construct->isPublic()) { + throw new NotFoundException( + sprintf('Alias (%s) has a non-public constructor and therefore cannot be instantiated', $id) + ); + } + + $resolution = $construct === null + ? new $id() + : $reflector->newInstanceArgs($this->reflectArguments($construct, $args)) + ; + + if ($this->cacheResolutions === true) { + $this->cache[$id] = $resolution; + } + + return $resolution; + } + + public function has($id): bool + { + return class_exists($id); + } + + public function call(callable $callable, array $args = []) + { + if (is_string($callable) && strpos($callable, '::') !== false) { + $callable = explode('::', $callable); + } + + if (is_array($callable)) { + if (is_string($callable[0])) { + // if we have a definition container, try that first, otherwise, reflect + try { + $callable[0] = $this->getContainer()->get($callable[0]); + } catch (ContainerException $e) { + $callable[0] = $this->get($callable[0]); + } + } + + $reflection = new ReflectionMethod($callable[0], $callable[1]); + + if ($reflection->isStatic()) { + $callable[0] = null; + } + + return $reflection->invokeArgs($callable[0], $this->reflectArguments($reflection, $args)); + } + + if (is_object($callable)) { + $reflection = new ReflectionMethod($callable, '__invoke'); + return $reflection->invokeArgs($callable, $this->reflectArguments($reflection, $args)); + } + + $reflection = new ReflectionFunction(\Closure::fromCallable($callable)); + + return $reflection->invokeArgs($this->reflectArguments($reflection, $args)); + } +} diff --git a/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php b/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php new file mode 100644 index 0000000000..6b69913147 --- /dev/null +++ b/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php @@ -0,0 +1,28 @@ +identifier ?? get_class($this); + } + + public function setIdentifier(string $id): ServiceProviderInterface + { + $this->identifier = $id; + return $this; + } +} diff --git a/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php b/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php new file mode 100644 index 0000000000..986091e2c0 --- /dev/null +++ b/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php @@ -0,0 +1,16 @@ +providers, true)) { + return $this; + } + + $provider->setContainer($this->getContainer()); + + if ($provider instanceof BootableServiceProviderInterface) { + $provider->boot(); + } + + $this->providers[] = $provider; + return $this; + } + + public function provides(string $service): bool + { + foreach ($this->getIterator() as $provider) { + if ($provider->provides($service)) { + return true; + } + } + + return false; + } + + public function getIterator(): Generator + { + yield from $this->providers; + } + + public function register(string $service): void + { + if (false === $this->provides($service)) { + throw new ContainerException( + sprintf('(%s) is not provided by a service provider', $service) + ); + } + + foreach ($this->getIterator() as $provider) { + if (in_array($provider->getIdentifier(), $this->registered, true)) { + continue; + } + + if ($provider->provides($service)) { + $provider->register(); + $this->registered[] = $provider->getIdentifier(); + } + } + } +} diff --git a/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php b/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php new file mode 100644 index 0000000000..c66a3b8362 --- /dev/null +++ b/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php @@ -0,0 +1,15 @@ + Date: Mon, 28 Oct 2024 12:15:43 +0600 Subject: [PATCH 06/12] Enhancement: Make analytics reports compatible with Dokan (#2318) * refactor: separate test directory for PHP and add factories * Add docs for factories * Update docs for test running instruction * Fix formating * refactor: folder structure for PSR4. * Fix composer autoload for test cases * Fix test auto loading * refactor: Simplify the shipping item attributes settings * Add docblock to DBAssertionTrait * Implement league container to swap Dokan curent container * Implement queue handling * Seperate file for WeDevs_Dokan class * Update Docblocks * Fix get & post request methods * Apply conditions on Query * Add FilterQuery class * Add brain monkey for mocking * Add test cases for ScheduleListener and FilteQuery of dokan_order_stats * Add PHPUnit data provider method for dokan_multi_vendor_order * Add test cases for order stats * Add create_multi_vendor_order method to DokanUnitTestCase * Update test cases * Check isset seller_id index * Remove dokan data retantion * Use default data only when given data is empty for multi vendor order * Add docs for TDD * Update docs style * Fix the ref in Docs * Add unit test only props * Update docs * Fix prop name * Fix base test class * Rename col is_sub_order to order_type * Add method to get order type * Apply Dokan stats conditions for Orders analytics * Inroduce Single vendor order refund type * Add where clause for refund * Add base query filter * Add query for condition for products analutics * Add test cases for Product analytics * Implement categories, variations and product segmenters reports * Implement tax report * Add docblocks * Refactor and add docblocks * Add group for analytics test cases * Alter WC order stats data store * Remove error log for debug * Implement coupons report only based on WC Order only * Fix plugin activation hooks * Add vendor hooks * Apply condition on SELECT clause to show coupon data against Dokan Sub-order * Implement customer analynics report * Update docs * Add seller filter * Update query param name for seller filter * Implement stock report product filter for seller * Change accessibility to get seller id * Implement stock stats report to filter by seller * Fix typo * Prevent the removal of child order insertion in WC order stats table * Pass valid filter param * Insert dokan stats data from the Order * Add col for shipping fee in dokan stats * Create dokan order stats table if not exists * Add Analytics Migrations * Update comment for ordery_type table column * Remove auto table creation * Remove un-related service provider * Add container documentation * Fix for refund order. * Fix shipping fee for Refund * Rename seller to vendor (#2416) * Rename seller to venor * Comment var dump * refactor: remove duplicacy * Update variable uses inside string --- docs/analytics/reports.md | 19 ++ .../Analytics/Reports/BaseQueryFilter.php | 179 ++++++++++ .../Reports/Categories/QueryFilter.php | 33 ++ .../Reports/Categories/Stats/QueryFilter.php | 22 ++ .../Analytics/Reports/Coupons/QueryFilter.php | 66 ++++ .../Reports/Coupons/Stats/QueryFilter.php | 40 +++ .../Reports/Coupons/Stats/WcDataStore.php | 28 ++ .../Reports/Customers/QueryFilter.php | 65 ++++ .../Reports/Customers/Stats/QueryFilter.php | 81 +++++ .../Analytics/Reports/DataStoreModifier.php | 58 ++++ includes/Analytics/Reports/OrderType.php | 193 +++++++++++ .../Analytics/Reports/Orders/QueryFilter.php | 91 +++++ .../Reports/Orders/Stats/DataStore.php | 259 +++++++++++++++ .../Reports/Orders/Stats/QueryFilter.php | 90 +++++ .../Reports/Orders/Stats/ScheduleListener.php | 52 +++ .../Reports/Orders/Stats/WcDataStore.php | 28 ++ .../Reports/Products/QueryFilter.php | 33 ++ .../Reports/Products/Stats/QueryFilter.php | 22 ++ .../Reports/Products/Stats/WcDataStore.php | 27 ++ .../Analytics/Reports/Stock/QueryFilter.php | 53 +++ .../Reports/Stock/Stats/WcDataStore.php | 166 ++++++++++ .../Analytics/Reports/Taxes/QueryFilter.php | 33 ++ .../Reports/Taxes/Stats/QueryFilter.php | 46 +++ .../Reports/Taxes/Stats/WcDataStore.php | 28 ++ .../Reports/Variations/QueryFilter.php | 33 ++ .../Reports/Variations/Stats/QueryFilter.php | 22 ++ includes/Analytics/Reports/WcSqlQuery.php | 19 ++ .../Providers/AnalyticsServiceProvider.php | 65 ++++ .../Providers/ServiceProvider.php | 1 + includes/Install/Installer.php | 28 ++ includes/Order/MiscHooks.php | 71 ---- includes/Upgrade/Manager.php | 3 +- includes/Upgrade/Upgrades.php | 1 + includes/Upgrade/Upgrades/V_3_13_0.php | 17 + .../Reports/OrderQueryFilterTest.php | 310 ++++++++++++++++++ .../Reports/OrderStatsQueryFilterTest.php | 166 ++++++++++ .../OrderStatsScheduleListenerTest.php | 83 +++++ .../src/Analytics/Reports/OrderTypeTest.php | 95 ++++++ .../Reports/ProductQueryFilterTest.php | 173 ++++++++++ .../Reports/ProductStatsQueryFilterTest.php | 169 ++++++++++ .../src/Analytics/Reports/ReportTestCase.php | 113 +++++++ .../Reports/StockProductQueryFilterTest.php | 54 +++ .../Reports/StockStatsQueryFilterTest.php | 53 +++ tests/php/src/DBAssertionTrait.php | 77 +++++ 44 files changed, 3192 insertions(+), 73 deletions(-) create mode 100644 docs/analytics/reports.md create mode 100644 includes/Analytics/Reports/BaseQueryFilter.php create mode 100644 includes/Analytics/Reports/Categories/QueryFilter.php create mode 100644 includes/Analytics/Reports/Categories/Stats/QueryFilter.php create mode 100644 includes/Analytics/Reports/Coupons/QueryFilter.php create mode 100644 includes/Analytics/Reports/Coupons/Stats/QueryFilter.php create mode 100644 includes/Analytics/Reports/Coupons/Stats/WcDataStore.php create mode 100644 includes/Analytics/Reports/Customers/QueryFilter.php create mode 100644 includes/Analytics/Reports/Customers/Stats/QueryFilter.php create mode 100644 includes/Analytics/Reports/DataStoreModifier.php create mode 100644 includes/Analytics/Reports/OrderType.php create mode 100644 includes/Analytics/Reports/Orders/QueryFilter.php create mode 100644 includes/Analytics/Reports/Orders/Stats/DataStore.php create mode 100644 includes/Analytics/Reports/Orders/Stats/QueryFilter.php create mode 100644 includes/Analytics/Reports/Orders/Stats/ScheduleListener.php create mode 100644 includes/Analytics/Reports/Orders/Stats/WcDataStore.php create mode 100644 includes/Analytics/Reports/Products/QueryFilter.php create mode 100644 includes/Analytics/Reports/Products/Stats/QueryFilter.php create mode 100644 includes/Analytics/Reports/Products/Stats/WcDataStore.php create mode 100644 includes/Analytics/Reports/Stock/QueryFilter.php create mode 100644 includes/Analytics/Reports/Stock/Stats/WcDataStore.php create mode 100644 includes/Analytics/Reports/Taxes/QueryFilter.php create mode 100644 includes/Analytics/Reports/Taxes/Stats/QueryFilter.php create mode 100644 includes/Analytics/Reports/Taxes/Stats/WcDataStore.php create mode 100644 includes/Analytics/Reports/Variations/QueryFilter.php create mode 100644 includes/Analytics/Reports/Variations/Stats/QueryFilter.php create mode 100644 includes/Analytics/Reports/WcSqlQuery.php create mode 100644 includes/DependencyManagement/Providers/AnalyticsServiceProvider.php create mode 100644 includes/Upgrade/Upgrades/V_3_13_0.php create mode 100644 tests/php/src/Analytics/Reports/OrderQueryFilterTest.php create mode 100644 tests/php/src/Analytics/Reports/OrderStatsQueryFilterTest.php create mode 100644 tests/php/src/Analytics/Reports/OrderStatsScheduleListenerTest.php create mode 100644 tests/php/src/Analytics/Reports/OrderTypeTest.php create mode 100644 tests/php/src/Analytics/Reports/ProductQueryFilterTest.php create mode 100644 tests/php/src/Analytics/Reports/ProductStatsQueryFilterTest.php create mode 100644 tests/php/src/Analytics/Reports/ReportTestCase.php create mode 100644 tests/php/src/Analytics/Reports/StockProductQueryFilterTest.php create mode 100644 tests/php/src/Analytics/Reports/StockStatsQueryFilterTest.php create mode 100644 tests/php/src/DBAssertionTrait.php diff --git a/docs/analytics/reports.md b/docs/analytics/reports.md new file mode 100644 index 0000000000..0b7b07ef2a --- /dev/null +++ b/docs/analytics/reports.md @@ -0,0 +1,19 @@ +- [Introduction](#introduction) +- [Custom Products Stats Datastore](#custom-products-stats-datastore) + +## Introduction +To handle **Dokan Orders**, we followed the [WooCommerce Admin Reports Extension Guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/docs/reporting/extending-woocommerce-admin-reports.md#handle-currency-parameters-on-the-server). + +## Custom Stats Datastore + +We need to customize the default *WooCommerce Analytics Datastore* for some reports. For example, we replaced the [WC Products Stats DataStore](https://github.com/woocommerce/woocommerce/blob/9297409c5a705d1cd0ae65ec9b058271bd90851e/plugins/woocommerce/src/Admin/API/Reports/Products/Stats/DataStore.php#L170) with the [Dokan Product Stats Store](./../../includes/Analytics/Reports/Products/Stats/WcDataStore.php). This modification involves overriding the `$total_query` and `$interval_query` properties by substituting the `Automattic\WooCommerce\Admin\API\Reports\SqlQuery` class with `WeDevs\Dokan\Analytics\Reports\WcSqlQuery`. + +The primary change was to update the `get_sql_clause( $type, $handling = 'unfiltered' )` method to `get_sql_clause( $type, $handling = '' )`, allowing us to apply necessary filters for adding JOIN and WHERE clauses to the `dokan_order_stats` table. + +### Implementation Steps + +- **Step 1:** Create the [WcSqlQuery](./../../includes/Analytics/Reports/DataStoreModifier.php) class to override the `get_sql_clause( $type, $handling = 'unfiltered' )` method from the [WC SqlQuery](https://github.com/woocommerce/woocommerce/blob/9297409c5a705d1cd0ae65ec9b058271bd90851e/plugins/woocommerce/src/Admin/API/Reports/SqlQuery.php#L87) class. The new method should use `get_sql_clause( $type, $handling = '' )`. + +- **Step 2:** Implement the [WcDataStore](https://github.com/woocommerce/woocommerce/blob/9297409c5a705d1cd0ae65ec9b058271bd90851e/plugins/woocommerce/src/Admin/API/Reports/Products/Stats/DataStore.php#L170) class to set the `$total_query` and `$interval_query` properties with instance of **WcSqlQuery**. + +- **Step 3:** Use the `woocommerce_data_stores` filter within the [DataStoreModifier](./../../includes/Analytics/Reports/DataStoreModifier.php) class to replace the default WooCommerce Products Stats datastore with the custom Dokan Product Stats Store. \ No newline at end of file diff --git a/includes/Analytics/Reports/BaseQueryFilter.php b/includes/Analytics/Reports/BaseQueryFilter.php new file mode 100644 index 0000000000..72e92968e1 --- /dev/null +++ b/includes/Analytics/Reports/BaseQueryFilter.php @@ -0,0 +1,179 @@ +register_hooks(); + } + + /** + * Add join clause for Dokan order state table in WooCommerce analytics queries. + * + * @param array $clauses The existing join clauses. + * + * @return array The modified join clauses. + */ + public function add_join_subquery( array $clauses ): array { + global $wpdb; + + $dokan_order_state_table = $this->get_dokan_table(); + + $clauses[] = "JOIN {$dokan_order_state_table} ON {$wpdb->prefix}{$this->wc_table}.order_id = {$dokan_order_state_table}.order_id"; + + return array_unique( $clauses ); + } + + /** + * Add where clause for Dokan order state in WooCommerce analytics queries. + * + * @param array $clauses The existing where clauses. + * + * @return array The modified where clauses. + */ + public function add_where_subquery( array $clauses ): array { + $dokan_order_state_table = $this->get_dokan_table(); + $order_types = $this->get_order_and_refund_types_to_include(); + + $clauses[] = "AND {$dokan_order_state_table}.order_type in ( $order_types ) "; + + $clauses = $this->add_where_subquery_for_refund( $clauses ); + $clauses = $this->add_where_subquery_for_vendor_filter( $clauses ); + + return array_unique( $clauses ); + } + + /** + * Add where clause for refunds in WooCommerce analytics queries. + * + * @param array $clauses The existing where clauses. + * + * @return array The modified where clauses. + */ + protected function add_where_subquery_for_refund( array $clauses ): array { + if ( ! isset( $_GET['refunds'] ) ) { + return $clauses; + } + + $dokan_order_state_table = $this->get_dokan_table(); + $order_types = $this->get_refund_types_to_include(); + + $clauses[] = "AND {$dokan_order_state_table}.order_type in ( $order_types ) "; + + return $clauses; + } + + /** + * Determine if the query should be filtered by seller ID. + * + * @return bool True if the query should be filtered by seller ID, false otherwise. + */ + public function should_filter_by_vendor_id(): bool { + return true; + } + + /** + * Get the order types to include in WooCommerce analytics queries. + * + * @return string The order types to include. + */ + protected function get_order_and_refund_types_to_include(): string { + $order_type = new OrderType(); + + if ( $this->should_filter_by_vendor_id() ) { + return implode( ',', $order_type->get_vendor_order_types() ); + } + + return implode( ',', $order_type->get_admin_order_types() ); + } + + /** + * Get the refund types to include in WooCommerce analytics queries. + * + * @return string The refund types to include. + */ + protected function get_refund_types_to_include(): string { + $order_type = new OrderType(); + + if ( $this->should_filter_by_vendor_id() ) { + return implode( ',', $order_type->get_vendor_refund_types() ); + } + + return implode( ',', $order_type->get_admin_refund_types() ); + } + + protected function get_dokan_table(): string { + return DataStore::get_db_table_name(); + } + + /** + * Get the non refund order types to include in WooCommerce analytics queries. + * + * @return string The refund types to include. + */ + protected function get_order_types_for_sql_excluding_refunds(): string { + $order_type = new OrderType(); + + if ( $this->should_filter_by_vendor_id() ) { + return implode( ',', $order_type->get_vendor_order_types_excluding_refunds() ); + } + + return implode( ',', $order_type->get_admin_order_types_excluding_refunds() ); + } + + /** + * Add where clause for seller query filter in WooCommerce analytics queries. + * + * @param array $clauses The existing where clauses. + * + * @return array The modified where clauses. + */ + protected function add_where_subquery_for_vendor_filter( array $clauses ): array { + $vendor_id = $this->get_vendor_id(); + + if ( ! $vendor_id ) { + return $clauses; + } + + $dokan_order_state_table = $this->get_dokan_table(); + + global $wpdb; + + $clauses[] = $wpdb->prepare( "AND {$dokan_order_state_table}.vendor_id = %s", $vendor_id ); //phpcs:ignore + + return $clauses; + } + + /** + * Get seller id from Query param for Admin and currently logged in user as Vendor + * + * @return int + */ + public function get_vendor_id() { + if ( ! is_user_logged_in() ) { + return 0; + } + + if ( ! current_user_can( 'manage_options' ) ) { + return dokan_get_current_user_id(); + } + + return (int) ( wp_unslash( $_GET['sellers'] ?? 0 ) ); // phpcs:ignore + } +} diff --git a/includes/Analytics/Reports/Categories/QueryFilter.php b/includes/Analytics/Reports/Categories/QueryFilter.php new file mode 100644 index 0000000000..d5428315b9 --- /dev/null +++ b/includes/Analytics/Reports/Categories/QueryFilter.php @@ -0,0 +1,33 @@ +context ) { + return $column; + } + + $order_type = new OrderType(); + $vendor_types = implode( ',', $order_type->get_vendor_order_types() ); + $admin_types = implode( ',', $order_type->get_admin_order_types() ); + $table_name = $this->get_dokan_table(); + + /** + * Parent order ID need to be set to 0 for Dokan suborder. + * Because, WC orders analytics generates order details link against the parent order + * assuming the order having parent ID as a refund order. + */ + // $column['parent_id'] = "(CASE WHEN {$table_name}.order_type NOT IN ($refund_types) THEN 0 ELSE {$wc_table_name}.parent_id END ) as parent_id"; + $column['amount'] = "SUM(CASE WHEN {$table_name}.order_type IN ($admin_types) THEN discount_amount ELSE 0 END ) as amount"; + $column['orders_count'] = "COUNT( DISTINCT (CASE WHEN {$table_name}.order_type IN ($vendor_types) THEN {$table_name}.order_id END) ) as orders_count"; + + return $column; + } +} diff --git a/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php b/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php new file mode 100644 index 0000000000..9c1e33c560 --- /dev/null +++ b/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php @@ -0,0 +1,40 @@ +clear_all_clauses(); + unset( $this->subquery ); + $this->total_query = new WcSqlQuery( $this->context . '_total' ); + $this->total_query->add_sql_clause( 'from', self::get_db_table_name() ); + + $this->interval_query = new WcSqlQuery( $this->context . '_interval' ); + $this->interval_query->add_sql_clause( 'from', self::get_db_table_name() ); + $this->interval_query->add_sql_clause( 'group_by', 'time_interval' ); + } +} diff --git a/includes/Analytics/Reports/Customers/QueryFilter.php b/includes/Analytics/Reports/Customers/QueryFilter.php new file mode 100644 index 0000000000..7fcdf72694 --- /dev/null +++ b/includes/Analytics/Reports/Customers/QueryFilter.php @@ -0,0 +1,65 @@ +context ) { + return $column; + } + + $types = $this->get_order_and_refund_types_to_include(); + $table_name = $this->get_dokan_table(); + + $orders_count = "COUNT( DISTINCT (CASE WHEN {$table_name}.order_type IN ($types) THEN {$table_name}.order_id END) ) "; //'SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END )'; + $total_spend = "SUM(CASE WHEN {$table_name}.order_type IN ($types) THEN total_sales ELSE 0 END )"; //'SUM( total_sales )'; + + /** + * Parent order ID need to be set to 0 for Dokan suborder. + * Because, WC orders analytics generates order details link against the parent order + * assuming the order having parent ID as a refund order. + */ + $column['orders_count'] = "{$orders_count} as orders_count"; + $column['total_spend'] = "{$total_spend} as total_spend"; + $column['avg_order_value'] = "CASE WHEN {$orders_count} = 0 THEN NULL ELSE {$total_spend} / {$orders_count} END AS avg_order_value"; + + return $column; + } +} diff --git a/includes/Analytics/Reports/Customers/Stats/QueryFilter.php b/includes/Analytics/Reports/Customers/Stats/QueryFilter.php new file mode 100644 index 0000000000..3a10e7b1ca --- /dev/null +++ b/includes/Analytics/Reports/Customers/Stats/QueryFilter.php @@ -0,0 +1,81 @@ +modify_select_field( $clause ); + } + + return $modified_clauses; + } + + protected function modify_select_field( string $field ): string { + $parts = explode( ' as ', strtolower( $field ) ); + $renamed_field = trim( $parts[1] ?? '' ); + + // The following fields need be modified. + // [0] => SUM( total_sales ) AS total_spend, + // [1] => SUM( CASE WHEN parent_id = 0 THEN 1 END ) as orders_count, + // [2] => CASE WHEN SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) = 0 THEN NULL ELSE SUM( total_sales ) / SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) END AS avg_order_value + + $table_name = $this->get_dokan_table(); + + $types = $this->get_order_and_refund_types_to_include(); + + switch ( str_replace( ',', '', $renamed_field ) ) { + case 'total_spend': + $field = "SUM(CASE WHEN {$table_name}.order_type IN ($types) THEN total_sales ELSE 0 END ) as total_spend"; + break; + case 'orders_count': + $field = "COUNT( DISTINCT (CASE WHEN {$table_name}.order_type IN ($types) THEN {$table_name}.order_id END) ) as orders_count"; + break; + case 'avg_order_value': + $order_types_conditions = "CASE WHEN {$table_name}.order_type IN ($types) THEN 1 ELSE 0 END"; + + $field = "CASE WHEN SUM( $order_types_conditions ) = 0 THEN NULL ELSE SUM( total_sales ) / SUM( $order_types_conditions ) END AS avg_order_value"; + break; + default: + break; + } + + // Append comma if the given field ends with comma[","]. + if ( str_ends_with( $renamed_field, ',' ) ) { + $field = $field . ','; + } + + return $field; + } +} diff --git a/includes/Analytics/Reports/DataStoreModifier.php b/includes/Analytics/Reports/DataStoreModifier.php new file mode 100644 index 0000000000..2048614054 --- /dev/null +++ b/includes/Analytics/Reports/DataStoreModifier.php @@ -0,0 +1,58 @@ +register_hooks(); + } + + public function register_hooks(): void { + add_filter( 'woocommerce_data_stores', [ $this, 'modify_wc_products_stats_datastore' ], 20 ); + } + + /** + * Customize the WooCommerce products stats datastore to override the $total_query and $interval_query properties. + * This modification replaces the Automattic\WooCommerce\Admin\API\Reports\SqlQuery class with WeDevs\Dokan\Analytics\Reports\WcSqlQuery + * to apply specific filters to queries. + * The reason for this change is that the "get_sql_clause" method's second parameter defaults to "unfiltered," which blocks the filters we need + * to add JOIN and WHERE clauses for the dokan_order_stats table. + * + * @see https://github.com/woocommerce/woocommerce/blob/9297409c5a705d1cd0ae65ec9b058271bd90851e/plugins/woocommerce/src/Admin/API/Reports/Products/Stats/DataStore.php#L170 + * + * @param array $wc_stores An array of WooCommerce datastores. + * @return array Modified array of WooCommerce datastores. + */ + public function modify_wc_products_stats_datastore( $wc_stores ) { + if ( isset( $wc_stores['report-products-stats'] ) ) { + $wc_stores['report-products-stats'] = \WeDevs\Dokan\Analytics\Reports\Products\Stats\WcDataStore::class; + } + + if ( isset( $wc_stores['report-taxes-stats'] ) ) { + $wc_stores['report-taxes-stats'] = \WeDevs\Dokan\Analytics\Reports\Taxes\Stats\WcDataStore::class; + } + + if ( isset( $wc_stores['report-orders-stats'] ) ) { + $wc_stores['report-orders-stats'] = \WeDevs\Dokan\Analytics\Reports\Orders\Stats\WcDataStore::class; + } + + if ( isset( $wc_stores['report-coupons-stats'] ) ) { + $wc_stores['report-coupons-stats'] = \WeDevs\Dokan\Analytics\Reports\Coupons\Stats\WcDataStore::class; + } + + if ( isset( $wc_stores['report-stock-stats'] ) ) { + $wc_stores['report-stock-stats'] = \WeDevs\Dokan\Analytics\Reports\Stock\Stats\WcDataStore::class; + } + + return $wc_stores; + } +} diff --git a/includes/Analytics/Reports/OrderType.php b/includes/Analytics/Reports/OrderType.php new file mode 100644 index 0000000000..47b4691b16 --- /dev/null +++ b/includes/Analytics/Reports/OrderType.php @@ -0,0 +1,193 @@ +get_parent_id() ) { + return false; + } + + if ( $order instanceof \WC_Order ) { + return true; + } + + $parent_order = wc_get_order( $order->get_parent_id() ); + + return $this->is_dokan_suborder_related( $parent_order ); + } + + /** + * Determines the type of the given order based on its relation to Dokan suborders and refunds. + * + * @param \WC_Abstract_Order $order The order object to classify. + * + * @return int The order type constant. + */ + public function get_type( \WC_Abstract_Order $order ): int { + $is_suborder_related = $this->is_dokan_suborder_related( $order ); + + if ( $is_suborder_related ) { + // Refund of Dokan suborder. + if ( $order instanceof WC_Order_Refund ) { + return self::DOKAN_SUBORDER_REFUND; + } + + // Dokan Suborder + return self::DOKAN_SUBORDER; + } + + if ( ! $is_suborder_related ) { + // Refund of WC order. + if ( $order instanceof WC_Order_Refund ) { + $suborder_ids = array_filter( + (array) dokan_get_suborder_ids_by( $order->get_parent_id() ) + ); + + if ( count( $suborder_ids ) ) { + return self::DOKAN_PARENT_ORDER_REFUND; + } + + return self::DOKAN_SINGLE_ORDER_REFUND; + } + + $suborder_ids = dokan_get_suborder_ids_by( $order->get_id() ); + + // Dokan Single Vendor Order + if ( $suborder_ids === null || ( is_array( $suborder_ids ) && count( $suborder_ids ) === 0 ) ) { + return self::DOKAN_SINGLE_ORDER; + } + } + + return self::DOKAN_PARENT_ORDER; + } + + /** + * Gets the list of order types relevant to admin users. + * + * @return array List of admin order type constants. + */ + public function get_admin_order_types(): array { + return [ + self::DOKAN_PARENT_ORDER, + self::DOKAN_SINGLE_ORDER, + self::DOKAN_PARENT_ORDER_REFUND, + self::DOKAN_SINGLE_ORDER_REFUND, + ]; + } + + /** + * Gets the list of order types relevant to sellers. + * + * @return array List of seller order type constants. + */ + public function get_vendor_order_types(): array { + return [ + self::DOKAN_SINGLE_ORDER, + self::DOKAN_SUBORDER, + self::DOKAN_SUBORDER_REFUND, + self::DOKAN_SINGLE_ORDER_REFUND, + ]; + } + + /** + * Gets the list of order types (excluding refunds) relevant to admin users. + * + * @return array List of admin order type constants (non-refund). + */ + public function get_admin_order_types_excluding_refunds(): array { + return [ + self::DOKAN_PARENT_ORDER, + self::DOKAN_SINGLE_ORDER, + ]; + } + + /** + * Gets the list of order types (excluding refunds) relevant to sellers. + * + * @return array List of seller order type constants (non-refund). + */ + public function get_vendor_order_types_excluding_refunds(): array { + return [ + self::DOKAN_SINGLE_ORDER, + self::DOKAN_SUBORDER, + ]; + } + + /** + * Gets the list of refund types relevant to all users. + * + * @return array List of refund type constants. + */ + public function get_refund_types(): array { + return [ + self::DOKAN_PARENT_ORDER_REFUND, + self::DOKAN_SUBORDER_REFUND, + self::DOKAN_SINGLE_ORDER_REFUND, + ]; + } + + /** + * Gets the list of refund types relevant to sellers. + * + * @return array List of seller refund type constants. + */ + public function get_vendor_refund_types(): array { + return [ + self::DOKAN_SUBORDER_REFUND, + self::DOKAN_SINGLE_ORDER_REFUND, + ]; + } + + /** + * Gets the list of refund types relevant to admin users. + * + * @return array List of admin refund type constants. + */ + public function get_admin_refund_types(): array { + return [ + self::DOKAN_PARENT_ORDER_REFUND, + self::DOKAN_SINGLE_ORDER_REFUND, + ]; + } + + /** + * Gets the list of refund types relevant to admin users. + * + * @return array List of admin refund type constants. + */ + public function get_all_order_types(): array { + return [ + self::DOKAN_PARENT_ORDER, + self::DOKAN_SINGLE_ORDER, + self::DOKAN_SUBORDER, + self::DOKAN_PARENT_ORDER_REFUND, + self::DOKAN_SUBORDER_REFUND, + self::DOKAN_SINGLE_ORDER_REFUND, + ]; + } +} diff --git a/includes/Analytics/Reports/Orders/QueryFilter.php b/includes/Analytics/Reports/Orders/QueryFilter.php new file mode 100644 index 0000000000..85d85d3944 --- /dev/null +++ b/includes/Analytics/Reports/Orders/QueryFilter.php @@ -0,0 +1,91 @@ +get_refund_types() ); + $table_name = Stats\DataStore::get_db_table_name(); + + /** + * Parent order ID need to be set to 0 for Dokan suborder. + * Because, WC orders analytics generates order details link against the parent order + * assuming the order having parent ID as a refund order. + */ + $column['parent_id'] = "(CASE WHEN {$table_name}.order_type NOT IN ($refund_types) THEN 0 ELSE {$wc_table_name}.parent_id END ) as parent_id"; + + return $column; + } + + /** + * Exclude order IDs from WooCommerce analytics queries based on seller or admin context. + * + * @param array $ids The existing excluded order IDs. + * @param array $query_args The query arguments. + * @param string $field The field being queried. + * @param string $context The context of the query. + * + * @return array The modified excluded order IDs. + */ + public function exclude_order_ids( array $ids, array $query_args, string $field, $context ): array { + if ( $context !== 'orders' || ! $this->should_filter_by_vendor_id() ) { + return $ids; + } + + return []; + } + + /** + * Add custom columns to the select clause of WooCommerce analytics queries. + * + * @param array $clauses The existing select clauses. + * + * @return array The modified select clauses. + */ + public function add_select_subquery( array $clauses ): array { + $clauses[] = ', vendor_earning, vendor_gateway_fee, vendor_discount, admin_commission, admin_gateway_fee, admin_discount, admin_subsidy'; + + return array_unique( $clauses ); + } +} diff --git a/includes/Analytics/Reports/Orders/Stats/DataStore.php b/includes/Analytics/Reports/Orders/Stats/DataStore.php new file mode 100644 index 0000000000..6428c3680a --- /dev/null +++ b/includes/Analytics/Reports/Orders/Stats/DataStore.php @@ -0,0 +1,259 @@ +date_column_name = get_option( 'woocommerce_date_type', 'date_paid' ); + parent::__construct(); + } + + /** + * Get the data based on args. + * + * @param array $args Query parameters. + * @return stdClass|WP_Error + */ + public function get_data( $args ) { + throw new Exception( 'Not supported by Dokan' ); + } + + /** + * Add order information to the lookup table when orders are created or modified. + * + * @param int $post_id Post ID. + * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success. + */ + public static function sync_order( $post_id ) { + $order = wc_get_order( $post_id ); + if ( ! $order ) { + return -1; + } + + return self::update( $order ); + } + + /** + * Update the database with stats data. + * + * @param \WC_Order|\WC_Order_Refund $order Order or refund to update row for. + * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success. + */ + public static function update( $order ) { + global $wpdb; + $table_name = self::get_db_table_name(); + + if ( ! $order->get_id() || ! $order->get_date_created() ) { + dokan_log( 'Dokan Analytics Order not found: ' . $order->get_id() ); + + return -1; + } + + // Following values are applicable for Refund Order. + $vendor_earning = 0; + $admin_earning = 0; + + $gateway_fee = 0; + $gateway_fee_provider = ''; + + $shipping_fee = $order->get_shipping_total(); + $shipping_fee_recipient = ''; + + // Override the values if order is a shop order. + switch ( $order->get_type() ) { + case 'shop_order': + $vendor_earning = (float) dokan()->commission->get_earning_by_order( $order ); + $admin_earning = (float) dokan()->commission->get_earning_by_order( $order, 'admin' ); + + $gateway_fee = $order->get_meta( 'dokan_gateway_fee' ); + $gateway_fee_provider = $order->get_meta( 'dokan_gateway_fee_paid_by' ); + $shipping_fee_recipient = $order->get_meta( 'shipping_fee_recipient' ); + break; + + case 'shop_order_refund': + $parent_order = wc_get_order( $order->get_parent_id() ); + $shipping_fee_recipient = $parent_order->get_meta( 'shipping_fee_recipient' ); + break; + default: + break; + } + + /** + * Filters order stats data. + * + * @param array $data Data written to order stats lookup table. + * @param WC_Order $order Order object. + * + * @since DOKAN_SINCE + */ + $data = apply_filters( + 'dokan_analytics_update_order_stats_data', + array( + 'order_id' => $order->get_id(), + 'vendor_id' => (int) self::get_vendor_id_from_order( $order ), + 'order_type' => (int) ( ( new OrderType() )->get_type( $order ) ), + // Seller Data + 'vendor_earning' => $vendor_earning, + 'vendor_gateway_fee' => $gateway_fee_provider === 'seller' ? $gateway_fee : '0', + 'vendor_shipping_fee' => $shipping_fee_recipient === 'seller' ? $shipping_fee : '0', + 'vendor_discount' => $order->get_meta( '_vendor_discount' ), + // Admin Data + 'admin_commission' => $admin_earning, + 'admin_gateway_fee' => $gateway_fee_provider !== 'seller' ? $gateway_fee : '0', + 'admin_shipping_fee' => $shipping_fee_recipient !== 'seller' ? $shipping_fee : '0', + 'admin_discount' => $order->get_meta( '_admin_discount' ), + 'admin_subsidy' => $order->get_meta( '_admin_subsidy' ), + ), + $order, + ); + + $format = array( + '%d', + '%d', + '%d', + // Seller data + '%f', + '%f', + '%f', + '%f', + // Admin data + '%f', + '%f', + '%f', + '%f', + '%f', + ); + + // Update or add the information to the DB. + $result = $wpdb->replace( $table_name, $data, $format ); + + /** + * Fires when Dokan order's stats reports are updated. + * + * @param int $order_id Order ID. + * + * @since DOKAN_SINCE + */ + do_action( 'dokan_analytics_update_order_stats', $order->get_id() ); + + // Check the rows affected for success. Using REPLACE can affect 2 rows if the row already exists. + return ( 1 === $result || 2 === $result ); + } + + /** + * Deletes the order stats when an order is deleted. + * + * @param int $post_id Post ID. + */ + public static function delete_order( $post_id ) { + global $wpdb; + $order_id = (int) $post_id; + + if ( ! OrderUtil::is_order( $post_id, array( 'shop_order', 'shop_order_refund' ) ) ) { + return; + } + + // Retrieve customer details before the order is deleted. + $order = wc_get_order( $order_id ); + $customer_id = absint( CustomersDataStore::get_existing_customer_id_from_order( $order ) ); + + // Delete the order. + $wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) ); + + /** + * Fires when orders stats are deleted. + * + * @param int $order_id Order ID. + * @param int $customer_id Customer ID. + * + * @since DOKAN_SINCE + */ + do_action( 'dokan_analytics_delete_order_stats', $order_id, $customer_id ); + } + + /** + * Gets the vendor ID associated with an order. + * + * @param \WC_Order $order Order object. + * + * @return int Vendor ID. + */ + protected static function get_vendor_id_from_order( $order ) { + $order_type = ( new OrderType() )->get_type( $order ); + + switch ( $order_type ) { + case OrderType::DOKAN_SUBORDER: + case OrderType::DOKAN_SINGLE_ORDER: + $vendor_id = $order->get_meta( '_dokan_vendor_id' ); + break; + case OrderType::DOKAN_SUBORDER_REFUND: + case OrderType::DOKAN_SINGLE_ORDER_REFUND: + $parent_order = wc_get_order( $order->get_parent_id() ); + $vendor_id = $parent_order->get_meta( '_dokan_vendor_id' ); + break; + default: + $vendor_id = 0; + break; + } + + return (int) $vendor_id; + } +} diff --git a/includes/Analytics/Reports/Orders/Stats/QueryFilter.php b/includes/Analytics/Reports/Orders/Stats/QueryFilter.php new file mode 100644 index 0000000000..4f0fe38a03 --- /dev/null +++ b/includes/Analytics/Reports/Orders/Stats/QueryFilter.php @@ -0,0 +1,90 @@ +context ) { + return $column; + } + + $table_name = $this->get_dokan_table(); + $types = $this->get_order_types_for_sql_excluding_refunds(); + + $order_count = "SUM( CASE WHEN {$table_name}.order_type IN($types) THEN 1 ELSE 0 END )"; + + $column['orders_count'] = "{$order_count} as orders_count"; + $column['avg_items_per_order'] = "SUM( {$wc_table_name}.num_items_sold ) / {$order_count} AS avg_items_per_order"; + $column['avg_order_value'] = "SUM( {$wc_table_name}.net_total ) / {$order_count} AS avg_order_value"; + $column['avg_admin_commission'] = "SUM( {$table_name}.admin_commission ) / {$order_count} AS avg_admin_commission"; + $column['avg_vendor_earning'] = "SUM( {$table_name}.vendor_earning ) / {$order_count} AS avg_vendor_earning"; + + return $column; + } + + /** + * Adds custom select subqueries for calculating Dokan-specific totals in the analytics reports. + * + * @param array $clauses The existing SQL select clauses. + * + * @return array Modified SQL select clauses. + */ + public function add_select_subquery_for_total( $clauses ) { + $table_name = $this->get_dokan_table(); + $types = $this->get_order_and_refund_types_to_include(); + + $clauses[] = ', sum(vendor_earning) as total_vendor_earning, sum(vendor_gateway_fee) as total_vendor_gateway_fee, sum(vendor_discount) as total_vendor_discount, sum(admin_commission) as total_admin_commission, sum(admin_gateway_fee) as total_admin_gateway_fee, sum(admin_discount) as total_admin_discount, sum(admin_subsidy) as total_admin_subsidy'; + $clauses[] = ", SUM( {$table_name}.admin_commission ) / SUM( CASE WHEN {$table_name}.order_type IN($types) THEN 1 ELSE 0 END ) AS avg_admin_commission"; + $clauses[] = ", SUM( {$table_name}.vendor_earning ) / SUM( CASE WHEN {$table_name}.order_type IN($types) THEN 1 ELSE 0 END ) AS avg_vendor_earning"; + + return $clauses; + } +} diff --git a/includes/Analytics/Reports/Orders/Stats/ScheduleListener.php b/includes/Analytics/Reports/Orders/Stats/ScheduleListener.php new file mode 100644 index 0000000000..d6390b7c8d --- /dev/null +++ b/includes/Analytics/Reports/Orders/Stats/ScheduleListener.php @@ -0,0 +1,52 @@ +register_hooks(); + } + + /** + * Register hooks for WooCommerce analytics order events. + * + * @return void + */ + public function register_hooks(): void { + add_action( 'woocommerce_analytics_update_order_stats', [ $this, 'sync_dokan_order' ] ); + add_action( 'woocommerce_analytics_delete_order_stats', [ $this, 'delete_order' ] ); + } + + /** + * Sync Dokan order data when WooCommerce analytics updates order stats. + * + * @param int $order_id The ID of the order being updated. + * + * @return void + */ + public function sync_dokan_order( $order_id ) { + return \WeDevs\Dokan\Analytics\Reports\Orders\Stats\DataStore::sync_order( $order_id ); + } + + /** + * Delete Dokan order data when WooCommerce deletes an order. + * + * @param int $order_id The ID of the order being deleted. + * + * @return void + */ + public function delete_order( $order_id ) { + return \WeDevs\Dokan\Analytics\Reports\Orders\Stats\DataStore::delete_order( $order_id ); + } +} diff --git a/includes/Analytics/Reports/Orders/Stats/WcDataStore.php b/includes/Analytics/Reports/Orders/Stats/WcDataStore.php new file mode 100644 index 0000000000..a098f4264d --- /dev/null +++ b/includes/Analytics/Reports/Orders/Stats/WcDataStore.php @@ -0,0 +1,28 @@ +clear_all_clauses(); + unset( $this->subquery ); + $this->total_query = new WcSqlQuery( $this->context . '_total' ); + $this->total_query->add_sql_clause( 'from', self::get_db_table_name() ); + + $this->interval_query = new WcSqlQuery( $this->context . '_interval' ); + $this->interval_query->add_sql_clause( 'from', self::get_db_table_name() ); + $this->interval_query->add_sql_clause( 'group_by', 'time_interval' ); + } +} diff --git a/includes/Analytics/Reports/Products/QueryFilter.php b/includes/Analytics/Reports/Products/QueryFilter.php new file mode 100644 index 0000000000..8548f1a20a --- /dev/null +++ b/includes/Analytics/Reports/Products/QueryFilter.php @@ -0,0 +1,33 @@ +clear_all_clauses(); + $this->total_query = new WcSqlQuery( $this->context . '_total' ); + $this->total_query->add_sql_clause( 'from', self::get_db_table_name() ); + + $this->interval_query = new WcSqlQuery( $this->context . '_interval' ); + $this->interval_query->add_sql_clause( 'from', self::get_db_table_name() ); + $this->interval_query->add_sql_clause( 'group_by', 'time_interval' ); + } +} diff --git a/includes/Analytics/Reports/Stock/QueryFilter.php b/includes/Analytics/Reports/Stock/QueryFilter.php new file mode 100644 index 0000000000..b196c0da4f --- /dev/null +++ b/includes/Analytics/Reports/Stock/QueryFilter.php @@ -0,0 +1,53 @@ +get_route() === '/wc-analytics/reports/stock' ) { + add_filter( 'posts_clauses', array( $this, 'add_author_clause' ), 10, 2 ); + } + + return $result; + } + + /** + * Apply seller ID query param to where SQL Clause. + * + * @param WP_Query $wp_query + * @return array + */ + public function add_author_clause( $args, $wp_query ) { + global $wpdb; + + $vendor_id = $this->get_vendor_id(); + + if ( $vendor_id ) { + $args['where'] = $args['where'] . $wpdb->prepare( + " AND {$wpdb->posts}.post_author = %d ", + $vendor_id + ); + } + + return $args; + } +} diff --git a/includes/Analytics/Reports/Stock/Stats/WcDataStore.php b/includes/Analytics/Reports/Stock/Stats/WcDataStore.php new file mode 100644 index 0000000000..1d2e4c344a --- /dev/null +++ b/includes/Analytics/Reports/Stock/Stats/WcDataStore.php @@ -0,0 +1,166 @@ +get_vendor_id(); + + $report_data = array(); + $cache_expire = DAY_IN_SECONDS * 30; + // Set seller specific key. + $low_stock_transient_name = 'wc_admin_stock_count_lowstock' . $vendor_id; + $low_stock_count = get_transient( $low_stock_transient_name ); + + if ( false === $low_stock_count ) { + $low_stock_count = $this->get_low_stock_count(); + set_transient( $low_stock_transient_name, $low_stock_count, $cache_expire ); + } else { + $low_stock_count = intval( $low_stock_count ); + } + + $report_data['lowstock'] = $low_stock_count; + + $status_options = wc_get_product_stock_status_options(); + foreach ( $status_options as $status => $label ) { + // Set seller specific key. + $transient_name = 'wc_admin_stock_count_' . $status . $vendor_id; + $count = get_transient( $transient_name ); + if ( false === $count ) { + $count = $this->get_count( $status ); + set_transient( $transient_name, $count, $cache_expire ); + } else { + $count = intval( $count ); + } + $report_data[ $status ] = $count; + } + + // Set seller specific key. + $product_count_transient_name = 'wc_admin_product_count' . $vendor_id; + $product_count = get_transient( $product_count_transient_name ); + if ( false === $product_count ) { + $product_count = $this->get_product_count(); + set_transient( $product_count_transient_name, $product_count, $cache_expire ); + } else { + $product_count = intval( $product_count ); + } + $report_data['products'] = $product_count; + return $report_data; + } + + /** + * Get low stock count (products with stock < low stock amount, but greater than no stock amount). + * + * @return int Low stock count. + */ + protected function get_low_stock_count() { + global $wpdb; + + $no_stock_amount = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) ); + $low_stock_amount = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) ); + $vendor_where = $this->get_vendor_where_query(); + + return (int) $wpdb->get_var( + $wpdb->prepare( + " + SELECT count( DISTINCT posts.ID ) FROM {$wpdb->posts} posts + LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id + LEFT JOIN {$wpdb->postmeta} low_stock_amount_meta ON posts.ID = low_stock_amount_meta.post_id AND low_stock_amount_meta.meta_key = '_low_stock_amount' + WHERE posts.post_type IN ( 'product', 'product_variation' ) + AND wc_product_meta_lookup.stock_quantity IS NOT NULL + AND wc_product_meta_lookup.stock_status = 'instock' + AND ( + ( + low_stock_amount_meta.meta_value > '' + AND wc_product_meta_lookup.stock_quantity <= CAST(low_stock_amount_meta.meta_value AS SIGNED) + AND wc_product_meta_lookup.stock_quantity > %d + ) + OR ( + ( + low_stock_amount_meta.meta_value IS NULL OR low_stock_amount_meta.meta_value <= '' + ) + AND wc_product_meta_lookup.stock_quantity <= %d + AND wc_product_meta_lookup.stock_quantity > %d + ) + ) + {$vendor_where} + ", + $no_stock_amount, + $low_stock_amount, + $no_stock_amount + ) + ); + } + + /** + * Get count for the passed in stock status. + * + * @param string $status Status slug. + * @return int Count. + */ + protected function get_count( $status ) { + global $wpdb; + + $vendor_where = $this->get_vendor_where_query(); + + return (int) $wpdb->get_var( + $wpdb->prepare(// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + " + SELECT count( DISTINCT posts.ID ) FROM {$wpdb->posts} posts + LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id + WHERE posts.post_type IN ( 'product', 'product_variation' ) + AND wc_product_meta_lookup.stock_status = %s {$vendor_where} + ", + $status + ) + ); + } + + /** + * Get product count for the store. + * + * @return int Product count. + */ + protected function get_product_count() { + $query_args = array(); + $query_args['post_type'] = array( 'product', 'product_variation' ); + $vendor_id = $this->get_vendor_id(); + + if ( $vendor_id ) { + $query_args['author'] = $vendor_id; + } + + $query = new \WP_Query(); + $query->query( $query_args ); + + return intval( $query->found_posts ); + } + + protected function get_vendor_id(): int { + return (int) dokan()->get_container()->get( \WeDevs\Dokan\Analytics\Reports\Stock\QueryFilter::class )->get_vendor_id(); + } + + protected function get_vendor_where_query() { + $vendor_id = $this->get_vendor_id(); + $where = ''; + + if ( $vendor_id ) { + global $wpdb; + + $where = $wpdb->prepare( + ' AND posts.post_author = %d ', + $vendor_id + ); + } + + return $where; + } +} diff --git a/includes/Analytics/Reports/Taxes/QueryFilter.php b/includes/Analytics/Reports/Taxes/QueryFilter.php new file mode 100644 index 0000000000..8dac5502da --- /dev/null +++ b/includes/Analytics/Reports/Taxes/QueryFilter.php @@ -0,0 +1,33 @@ +context ) { + return $column; + } + + $table_name = $this->get_dokan_table(); + $types = $this->get_order_types_for_sql_excluding_refunds(); + + $column['orders_count'] = "SUM( CASE WHEN {$table_name}.order_type IN ($types) THEN 1 ELSE 0 END ) as orders_count"; + + return $column; + } +} diff --git a/includes/Analytics/Reports/Taxes/Stats/WcDataStore.php b/includes/Analytics/Reports/Taxes/Stats/WcDataStore.php new file mode 100644 index 0000000000..357efba06a --- /dev/null +++ b/includes/Analytics/Reports/Taxes/Stats/WcDataStore.php @@ -0,0 +1,28 @@ +clear_all_clauses(); + unset( $this->subquery ); + $this->total_query = new WcSqlQuery( $this->context . '_total' ); + $this->total_query->add_sql_clause( 'from', self::get_db_table_name() ); + + $this->interval_query = new WcSqlQuery( $this->context . '_interval' ); + $this->interval_query->add_sql_clause( 'from', self::get_db_table_name() ); + $this->interval_query->add_sql_clause( 'group_by', 'time_interval' ); + } +} diff --git a/includes/Analytics/Reports/Variations/QueryFilter.php b/includes/Analytics/Reports/Variations/QueryFilter.php new file mode 100644 index 0000000000..16615bb88d --- /dev/null +++ b/includes/Analytics/Reports/Variations/QueryFilter.php @@ -0,0 +1,33 @@ +services, true ) || in_array( $alias, self::TAGS, true ); + } + + /** + * Register the classes. + */ + public function register(): void { + foreach ( $this->services as $service ) { + $definition = $this->getContainer() + ->addShared( + $service, function () use ( $service ) { + return new $service(); + } + ); + $this->add_tags( $definition, self::TAGS ); + } + } + + private function add_tags( DefinitionInterface $definition, $tags ) { + foreach ( $tags as $tag ) { + $definition = $definition->addTag( $tag ); + } + } +} diff --git a/includes/DependencyManagement/Providers/ServiceProvider.php b/includes/DependencyManagement/Providers/ServiceProvider.php index d7aa3f0b9d..1a465b5f64 100644 --- a/includes/DependencyManagement/Providers/ServiceProvider.php +++ b/includes/DependencyManagement/Providers/ServiceProvider.php @@ -48,6 +48,7 @@ public function boot(): void { $this->getContainer()->addServiceProvider( new CommonServiceProvider() ); $this->getContainer()->addServiceProvider( new FrontendServiceProvider() ); $this->getContainer()->addServiceProvider( new AjaxServiceProvider() ); + $this->getContainer()->addServiceProvider( new AnalyticsServiceProvider() ); } /** diff --git a/includes/Install/Installer.php b/includes/Install/Installer.php index ee3ceeb14c..1d11c51141 100755 --- a/includes/Install/Installer.php +++ b/includes/Install/Installer.php @@ -327,6 +327,7 @@ public function create_tables() { $this->create_refund_table(); $this->create_vendor_balance_table(); $this->create_reverse_withdrawal_table(); + $this->create_dokan_order_stats_table(); } /** @@ -536,4 +537,31 @@ private static function parse_update_notice( $content, $new_version ) { return wp_kses_post( $upgrade_notice ); } + + public function create_dokan_order_stats_table() { + // Following imported here because this method could be called from the others file. + include_once ABSPATH . 'wp-admin/includes/upgrade.php'; + + global $wpdb; + + $sql = "CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}dokan_order_stats` ( + `order_id` bigint UNSIGNED NOT NULL, + `vendor_id` bigint UNSIGNED NOT NULL DEFAULT '0', + `order_type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0 = Dokan Parent Order, 1 = Dokan Single Vendor Order, 2 = Dokan Suborder, 3 = Refund of Dokan Parent Order, 4 = Refund of Dokan Suborder, 5 = Refund of Dokan Single Order', + `vendor_earning` double NOT NULL DEFAULT '0', + `vendor_gateway_fee` double NOT NULL DEFAULT '0', + `vendor_shipping_fee` double NOT NULL DEFAULT '0', + `vendor_discount` double NOT NULL DEFAULT '0', + `admin_commission` double NOT NULL DEFAULT '0', + `admin_gateway_fee` double NOT NULL DEFAULT '0', + `admin_shipping_fee` double NOT NULL DEFAULT '0', + `admin_discount` double NOT NULL DEFAULT '0', + `admin_subsidy` double NOT NULL DEFAULT '0', + PRIMARY KEY (order_id), + KEY vendor_id (vendor_id), + KEY order_type (order_type) + ) ENGINE=InnoDB {$wpdb->get_charset_collate()};"; + + dbDelta( $sql ); + } } diff --git a/includes/Order/MiscHooks.php b/includes/Order/MiscHooks.php index a4b897530b..60cd36abc0 100644 --- a/includes/Order/MiscHooks.php +++ b/includes/Order/MiscHooks.php @@ -22,13 +22,6 @@ class MiscHooks { * @since 3.8.0 */ public function __construct() { - //Wc remove child order from wc_order_product_lookup & trim child order from posts for analytics - add_action( 'wc-admin_import_orders', [ $this, 'delete_child_order_from_wc_order_product' ] ); - - // Exclude suborders in woocommerce analytics. - add_filter( 'woocommerce_analytics_orders_select_query', [ $this, 'trim_child_order_for_analytics_order' ] ); - add_filter( 'woocommerce_analytics_update_order_stats_data', [ $this, 'trim_child_order_for_analytics_order_stats' ], 10, 2 ); - // remove customer info from order export based on setting add_filter( 'dokan_csv_export_headers', [ $this, 'hide_customer_info_from_vendor_order_export' ], 20, 1 ); @@ -36,46 +29,6 @@ public function __construct() { add_filter( 'wp_count_posts', [ $this, 'modify_vendor_order_counts' ], 10, 1 ); // no need to add hpos support for this filter } - /** - * Delete_child_order_from_wc_order_product - * - * @since 3.8.0 Moved this method from Order/Hooks.php file - * - * @param \ActionScheduler_Action $args - * - * @return void - */ - public function delete_child_order_from_wc_order_product( $args ) { - $order = wc_get_order( $args ); - - if ( $order->get_parent_id() ) { - global $wpdb; - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->delete( $wpdb->prefix . 'wc_order_product_lookup', [ 'order_id' => $order->get_id() ] ); - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->delete( $wpdb->prefix . 'wc_order_stats', [ 'order_id' => $order->get_id() ] ); - } - } - - /** - * Trim child order if parent exist from wc_order_product_lookup for analytics order - * - * @since 3.8.0 Moved this method from Order/Hooks.php file - * - * @param WC_Order $orders - * - * @return WC_Order - */ - public function trim_child_order_for_analytics_order( $orders ) { - foreach ( $orders->data as $key => $order ) { - if ( $order['parent_id'] ) { - unset( $orders->data[ $key ] ); - } - } - - return $orders; - } - /** * Remove customer sensitive information while exporting order * @@ -195,28 +148,4 @@ public function modify_vendor_order_counts( $counts ) { return $counts; } - - /** - * Exclude suborders and include dokan subscription product orders when generate woocommerce analytics data. - * - * @see https://github.com/getdokan/dokan-pro/issues/2735 - * - * @param array $data - * @param \WC_Order $order - * - * @return array - */ - public function trim_child_order_for_analytics_order_stats( $data, $order ) { - if ( ! $order->get_parent_id() || - ( - dokan()->is_pro_exists() - && dokan_pro()->module->is_active( 'product_subscription' ) - && \DokanPro\Modules\Subscription\Helper::is_vendor_subscription_order( $order ) - ) - ) { - return $data; - } - - return []; - } } diff --git a/includes/Upgrade/Manager.php b/includes/Upgrade/Manager.php index cb622d8efa..eae16fd326 100644 --- a/includes/Upgrade/Manager.php +++ b/includes/Upgrade/Manager.php @@ -32,7 +32,7 @@ public function is_upgrade_required() { * @return bool */ public function has_ongoing_process() { - return ! ! get_option( $this->is_upgrading_db_key, false ); + return (bool) get_option( $this->is_upgrading_db_key, false ); } /** @@ -112,4 +112,3 @@ public function do_upgrade() { do_action( 'dokan_upgrade_finished' ); } } - diff --git a/includes/Upgrade/Upgrades.php b/includes/Upgrade/Upgrades.php index f12d269472..a81110997f 100644 --- a/includes/Upgrade/Upgrades.php +++ b/includes/Upgrade/Upgrades.php @@ -43,6 +43,7 @@ class Upgrades { '3.6.5' => Upgrades\V_3_6_5::class, '3.7.10' => Upgrades\V_3_7_10::class, '3.7.19' => Upgrades\V_3_7_19::class, + '3.13.0' => Upgrades\V_3_13_0::class, ]; /** diff --git a/includes/Upgrade/Upgrades/V_3_13_0.php b/includes/Upgrade/Upgrades/V_3_13_0.php new file mode 100644 index 0000000000..3620a0c75a --- /dev/null +++ b/includes/Upgrade/Upgrades/V_3_13_0.php @@ -0,0 +1,17 @@ +create_dokan_order_stats_table(); + + // Sync the WC order stats. + $import = ReportsSync::regenerate_report_data( null, false ); + } +} diff --git a/tests/php/src/Analytics/Reports/OrderQueryFilterTest.php b/tests/php/src/Analytics/Reports/OrderQueryFilterTest.php new file mode 100644 index 0000000000..574b6520c9 --- /dev/null +++ b/tests/php/src/Analytics/Reports/OrderQueryFilterTest.php @@ -0,0 +1,310 @@ +sut = dokan_get_container()->get( QueryFilter::class ); + } + + protected function tearDown(): void { + parent::tearDown(); + + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $this->sut ); + } + /** + * Test that the order statistics hooks are registered correctly. + * + * @see https://giuseppe-mazzapica.gitbook.io/brain-monkey/wordpress-specific-tools/wordpress-hooks-added + * + * @return void + */ + public function test_orders_hook_registered() { + $order_stats_query_filter = dokan_get_container()->get( QueryFilter::class ); + // Assert the Join Clause filters are registered + self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_join_orders_subquery', [ $order_stats_query_filter, 'add_join_subquery' ] ) ); + // Assert the Where Clause filters are registered + self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $order_stats_query_filter, 'add_where_subquery' ] ) ); + } + + /** + * Test dokan_order_stats JOIN and WHERE clauses are applied on order_stats select Query. + * + * Method(partial) mocking @see http://docs.mockery.io/en/latest/reference/partial_mocks.html + * + * @param int $order_id The order ID. + * @param int $vendor_id1 The first seller ID. + * @param int $vendor_id2 The second seller ID. + * @return void + */ + public function test_filter_hooks_are_applied_for_orders_query() { + $order_id = $this->create_multi_vendor_order(); + + $this->run_all_pending(); + + $mocking_methods = [ + 'add_join_subquery', + 'add_where_subquery', + 'add_select_subquery', + ]; + + $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' ); + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service ); + + foreach ( $mocking_methods as $method ) { + $service->shouldReceive( $method ) + ->atLeast() + ->once() + ->andReturnUsing( + function ( $clauses ) { + return $clauses; + } + ); + } + + $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query( [], 'orders' ); + + $wc_stats_query->get_data(); + } + + /** + * @dataProvider get_dokan_stats_data + * + * @return void + */ + public function test_dokan_order_stats_fields_are_selected_for_seller( $expected_data ) { + $order_id = $this->create_multi_vendor_order(); + + $this->set_order_meta_for_dokan( $order_id, $expected_data ); + + $this->run_all_pending(); + + $mocking_methods = [ + 'should_filter_by_vendor_id', + ]; + + $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' ); + + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service ); + + remove_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $this->sut, 'add_where_subquery' ], 30 ); + + $service->shouldReceive( 'should_filter_by_vendor_id' ) + ->andReturnTrue(); + + $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query( [], 'orders' ); + + $data = $wc_stats_query->get_data(); + + $sub_ids = dokan_get_suborder_ids_by( $order_id ); + + $report_data = $data->data; + + $this->assertEquals( count( $sub_ids ), count( $report_data ) ); + + foreach ( $sub_ids as $index => $s_id ) { + $sub_order = wc_get_order( $s_id ); + $order_data = $report_data[ $index ]; + + $this->assertEquals( $s_id, $order_data['order_id'] ); + $this->assertEquals( floatval( $sub_order->get_total() ), $order_data['total_sales'] ); + + foreach ( $expected_data as $key => $val ) { + $this->assertEquals( $val, $order_data[ $key ] ); + } + } + } + + /** + * @dataProvider get_dokan_stats_data + * + * @return void + */ + public function test_dokan_order_stats_fields_are_selected_for_admin( $expected_data ) { + $order_id = $this->create_multi_vendor_order(); + + $this->set_order_meta_for_dokan( $order_id, $expected_data ); + + $this->run_all_pending(); + + remove_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $this->sut, 'add_where_subquery' ], 30 ); + + $service = Mockery::mock( QueryFilter::class . '[should_filter_by_vendor_id]' ); + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service ); + + $service->shouldReceive( 'should_filter_by_vendor_id' ) + ->andReturnUsing( + function () { + return false; + } + ); + + $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query( [], 'orders' ); + + $data = $wc_stats_query->get_data(); + + $sub_ids = dokan_get_suborder_ids_by( $order_id ); + + $report_data = $data->data; + + $this->assertEquals( 1, count( $report_data ) ); + + $report_data = $report_data[0]; + + foreach ( $expected_data as $key => $val ) { + $this->assertArrayHasKey( $key, $report_data ); + } + } + + public function test_orders_for_dokan_suborder_refund() { + $order_id = $this->create_multi_vendor_order(); + $sub_ids = dokan_get_suborder_ids_by( $order_id ); + + $refund = $this->create_refund( $sub_ids[0] ); + + $this->run_all_pending(); + + remove_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $this->sut, 'add_where_subquery' ], 30 ); + + $service = Mockery::mock( QueryFilter::class . '[should_filter_by_vendor_id]' ); + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service ); + + $service->shouldReceive( 'should_filter_by_vendor_id' ) + ->andReturnTrue(); + + $_GET['refunds'] = 'all'; + + $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query( [ 'refunds' => 'all' ], 'orders' ); + $data = $wc_stats_query->get_data(); + + $report_data = $data->data; + + $this->assertEquals( 1, count( $report_data ) ); + + $report_data = $report_data[0]; + + $this->assertEquals( $refund->get_id(), $report_data['order_id'] ); + } + + public function test_orders_for_dokan_parent_order_refund() { + $order_id = $this->create_multi_vendor_order(); + $sub_ids = dokan_get_suborder_ids_by( $order_id ); + + $parent_refund = $this->create_refund( $sub_ids[0], true, true ); + + $this->run_all_pending(); + + remove_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $this->sut, 'add_where_subquery' ], 30 ); + + $service = Mockery::mock( QueryFilter::class . '[should_filter_by_vendor_id]' ); + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service ); + + $service->shouldReceive( 'should_filter_by_vendor_id' ) + ->andReturnFalse(); + + $_GET['refunds'] = 'all'; + + $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query( [ 'refunds' => 'all' ], 'orders' ); + $data = $wc_stats_query->get_data(); + + $report_data = $data->data; + + $this->assertEquals( 1, count( $report_data ) ); + + $report_data = $report_data[0]; + + $this->assertEquals( $parent_refund->get_id(), $report_data['order_id'] ); + } + + public function test_orders_for_dokan_single_order_refund() { + $this->seller_id2 = $this->seller_id1; + + $order_id = $this->create_multi_vendor_order(); + + $refund = $this->create_refund( $order_id ); + + $this->run_all_pending(); + + remove_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $this->sut, 'add_where_subquery' ], 30 ); + + $service = Mockery::mock( QueryFilter::class . '[should_filter_by_vendor_id]' ); + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service ); + + $service->shouldReceive( 'should_filter_by_vendor_id' ) + ->andReturnFalse(); + + $_GET['refunds'] = 'all'; + + $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query( [ 'refunds' => 'all' ], 'orders' ); + $data = $wc_stats_query->get_data(); + + $report_data = $data->data; + + $this->assertEquals( 1, count( $report_data ) ); + + $report_data = $report_data[0]; + + $this->assertEquals( $refund->get_id(), $report_data['order_id'] ); + } + + public function test_orders_analytics_for_vendor_filter_as_a_admin() { + $order_id = $this->create_single_vendor_order( $this->seller_id1 ); + $order_id2 = $this->create_single_vendor_order( $this->seller_id2 ); + + $this->run_all_pending(); + + $_GET['sellers'] = $this->seller_id1; + + wp_set_current_user( $this->admin_id ); + + $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query( [], 'orders' ); + + $data = $wc_stats_query->get_data(); + + $sub_ids = dokan_get_suborder_ids_by( $order_id ); + + $report_data = $data->data; + + $this->assertEquals( 1, count( $report_data ) ); + $this->assertEquals( $order_id, $report_data[0]['order_id'] ); + } + + public function test_orders_analytics_for_vendor_filter_as_a_vendor() { + $order_id = $this->create_single_vendor_order( $this->seller_id1 ); + $order_id2 = $this->create_single_vendor_order( $this->seller_id2 ); + + $this->run_all_pending(); + + // Ignore seller filter if a seller pass another seller ID as filter . + $_GET['seller'] = $this->seller_id1; + + wp_set_current_user( $this->seller_id2 ); + + $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query( [], 'orders' ); + + $data = $wc_stats_query->get_data(); + + $sub_ids = dokan_get_suborder_ids_by( $order_id ); + + $report_data = $data->data; + + $this->assertEquals( 1, count( $report_data ) ); + $this->assertEquals( $order_id2, $report_data[0]['order_id'] ); + } +} diff --git a/tests/php/src/Analytics/Reports/OrderStatsQueryFilterTest.php b/tests/php/src/Analytics/Reports/OrderStatsQueryFilterTest.php new file mode 100644 index 0000000000..f9a338b1de --- /dev/null +++ b/tests/php/src/Analytics/Reports/OrderStatsQueryFilterTest.php @@ -0,0 +1,166 @@ +sut = dokan_get_container()->get( QueryFilter::class ); + } + + protected function tearDown(): void { + parent::tearDown(); + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $this->sut ); + } + + /** + * Test that the order statistics hooks are registered correctly. + * + * @see https://giuseppe-mazzapica.gitbook.io/brain-monkey/wordpress-specific-tools/wordpress-hooks-added + * + * @return void + */ + public function test_order_stats_hook_registered() { + $order_stats_query_filter = dokan_get_container()->get( QueryFilter::class ); + // Assert the Join Clause filters are registered + self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_join_orders_stats_total', [ $order_stats_query_filter, 'add_join_subquery' ] ) ); + self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_join_orders_stats_interval', [ $order_stats_query_filter, 'add_join_subquery' ] ) ); + // Assert the Where Clause filters are registered + self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_where_orders_stats_total', [ $order_stats_query_filter, 'add_where_subquery' ] ) ); + self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_where_orders_stats_interval', [ $order_stats_query_filter, 'add_where_subquery' ] ) ); + } + + + /** + * Test dokan_order_stats JOIN and WHERE clauses are applied on order_stats select Query. + * + * Method(partial) mocking @see http://docs.mockery.io/en/latest/reference/partial_mocks.html + * + * @param int $order_id The order ID. + * @param int $vendor_id1 The first seller ID. + * @param int $vendor_id2 The second seller ID. + * @return void + */ + public function test_dokan_order_states_query_filter_hooks_are_order_stats_update() { + $order_id = $this->create_multi_vendor_order(); + + $this->run_all_pending(); + + $mocking_methods = [ + 'add_join_subquery', + 'add_where_subquery', + 'add_select_subquery_for_total', + ]; + + $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' ); + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service ); + + foreach ( $mocking_methods as $method ) { + $service->shouldReceive( $method ) + ->atLeast() + ->once() + ->andReturnUsing( + function ( $clauses ) { + return $clauses; + } + ); + } + + $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Query( [], 'orders-stats' ); + + $wc_stats_query->get_data(); + } + + /** + * @dataProvider get_dokan_stats_data + * + * @return void + */ + public function test_dokan_order_stats_added_to_wc_select_query_for_seller( array $data ) { + $parent_id = $this->create_multi_vendor_order(); + + $this->set_order_meta_for_dokan( $parent_id, $data ); + + $this->run_all_pending(); + + $filter = Mockery::mock( QueryFilter::class . '[should_filter_by_vendor_id]' ); + + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $filter ); + + $filter->shouldReceive( 'should_filter_by_vendor_id' ) + ->atLeast() + ->once() + ->andReturnTrue(); + + $orders_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Query( [] ); + + $report_data = $orders_query->get_data(); + + $sub_ids = dokan_get_suborder_ids_by( $parent_id ); + + $this->assertCount( $report_data->totals->orders_count, $sub_ids ); + + $sub_ord_count = count( $sub_ids ); + + // Assert dokan order stats totals. + foreach ( $data as $key => $val ) { + $this->assertEquals( floatval( $val * $sub_ord_count ), $report_data->totals->{"total_$key"} ); + } + } + + /** + * @dataProvider get_dokan_stats_data + * + * @return void + */ + public function test_dokan_order_stats_added_to_wc_select_query_for_admin( array $data ) { + $parent_id = $this->create_multi_vendor_order(); + $this->set_order_meta_for_dokan( $parent_id, $data ); + + $this->run_all_pending(); + + $filter = Mockery::mock( QueryFilter::class . '[should_filter_by_vendor_id]' ); + + remove_filter( 'woocommerce_analytics_clauses_where_orders_stats_total', [ $this->sut, 'add_where_subquery' ], 30 ); + remove_filter( 'woocommerce_analytics_clauses_where_orders_stats_total', [ $this->sut, 'add_where_subquery' ], 30 ); + + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $filter ); + + $filter->shouldReceive( 'should_filter_by_vendor_id' ) + ->atLeast() + ->once() + ->andReturnFalse(); + + $orders_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Query( [], 'orders-stats' ); + + $report_data = $orders_query->get_data(); + + $sub_ids = dokan_get_suborder_ids_by( $parent_id ); + + $this->assertEquals( 1, $report_data->totals->orders_count ); + + $sub_ord_count = count( $sub_ids ); + // var_dump( $data ); + // Assert dokan order stats totals. + foreach ( $data as $key => $val ) { + $this->assertEquals( floatval( $val * $sub_ord_count ), $report_data->totals->{"total_$key"} ); + } + } +} diff --git a/tests/php/src/Analytics/Reports/OrderStatsScheduleListenerTest.php b/tests/php/src/Analytics/Reports/OrderStatsScheduleListenerTest.php new file mode 100644 index 0000000000..db400d50dc --- /dev/null +++ b/tests/php/src/Analytics/Reports/OrderStatsScheduleListenerTest.php @@ -0,0 +1,83 @@ +get( ScheduleListener::class ); + self::assertNotFalse( has_action( 'woocommerce_analytics_update_order_stats', [ $order_stats_table_listener, 'sync_dokan_order' ] ) ); + } + + /** + * Test the dokan_order_stats table update hooks are executed by ScheduleListener class. + * + * Method(partial) mocking @see http://docs.mockery.io/en/latest/reference/partial_mocks.html + * + * @param int $order_id The order ID. + * @param int $vendor_id1 The first seller ID. + * @param int $vendor_id2 The second seller ID. + * @return void + */ + public function test_dokan_order_states_update_hook_execute_on_order_stats_update() { + $order_id = $this->create_multi_vendor_order(); + $service = Mockery::mock( ScheduleListener::class . '[sync_dokan_order]' ); + dokan_get_container()->extend( ScheduleListener::class )->setConcrete( $service ); + + $service->shouldReceive( 'sync_dokan_order' ) + ->atLeast() + ->once() + ->andReturn( 1 ); + + $this->run_all_pending(); + } + + /** + * Test the mock class for multi-vendor order statistics. + * + * @param int $order_id The order ID. + * @param int $vendor_id1 The first seller ID. + * @param int $vendor_id2 The second seller ID. + * @return void + */ + public function test_data_is_inserted_in_dokan_order_stats_table() { + $order_id = $this->create_multi_vendor_order(); + + $this->run_all_pending(); + + $wc_order_stats_table = \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore::get_db_table_name(); + + $dokan_order_stats_table = \WeDevs\Dokan\Analytics\Reports\Orders\Stats\DataStore::get_db_table_name(); + + $this->assertDatabaseCount( + $wc_order_stats_table, 1, [ + 'order_id' => $order_id, + ] + ); + + $this->assertDatabaseCount( + $dokan_order_stats_table, 1, [ + 'order_id' => $order_id, + 'order_type' => OrderType::DOKAN_PARENT_ORDER, + ] + ); + + $sub_order_ids = dokan_get_suborder_ids_by( $order_id ); + + foreach ( $sub_order_ids as $sub_id ) { + $this->assertDatabaseCount( + $dokan_order_stats_table, 1, [ + 'order_id' => $sub_id, + 'order_type' => OrderType::DOKAN_SUBORDER, + ] + ); + } + } +} diff --git a/tests/php/src/Analytics/Reports/OrderTypeTest.php b/tests/php/src/Analytics/Reports/OrderTypeTest.php new file mode 100644 index 0000000000..8ef702e1a1 --- /dev/null +++ b/tests/php/src/Analytics/Reports/OrderTypeTest.php @@ -0,0 +1,95 @@ +sut = new OrderType(); + } + + public function test_wc_order_is_dokan_suborder_related_method() { + $parent_id = $this->create_multi_vendor_order(); + $parent_order = wc_get_order( $parent_id ); + + $this->assertFalse( $this->sut->is_dokan_suborder_related( $parent_order ) ); + + $sub_order_ids = dokan_get_suborder_ids_by( $parent_id ); + + foreach ( $sub_order_ids as $sub_id ) { + $sub_order = wc_get_order( $sub_id ); + + $this->assertTrue( $this->sut->is_dokan_suborder_related( $sub_order ) ); + } + } + + public function test_order_type_method_for_multi_vendor() { + $parent_id = $this->create_multi_vendor_order(); + $parent_order = wc_get_order( $parent_id ); + + $this->assertEquals( $this->sut::DOKAN_PARENT_ORDER, $this->sut->get_type( $parent_order ) ); + + $sub_order_ids = dokan_get_suborder_ids_by( $parent_id ); + + foreach ( $sub_order_ids as $sub_id ) { + $sub_order = wc_get_order( $sub_id ); + + $this->assertEquals( $this->sut::DOKAN_SUBORDER, $this->sut->get_type( $sub_order ) ); + } + } + + public function test_order_type_method_for_single_vendor() { + $this->seller_id2 = $this->seller_id1; + + $parent_id = $this->create_multi_vendor_order(); + $parent_order = wc_get_order( $parent_id ); + + $this->assertEquals( $this->sut::DOKAN_SINGLE_ORDER, $this->sut->get_type( $parent_order ) ); + + $sub_order_ids = dokan_get_suborder_ids_by( $parent_id ); + + $this->assertNull( $sub_order_ids ); + } + + public function test_order_type_for_dokan_sub_refund() { + $parent_id = $this->create_multi_vendor_order(); + + $sub_order_ids = dokan_get_suborder_ids_by( $parent_id ); + $sub_id = $sub_order_ids[0]; + + $refund = $this->create_refund( $sub_id ); + + $this->assertEquals( $this->sut::DOKAN_SUBORDER_REFUND, $this->sut->get_type( $refund ) ); + } + + public function test_order_type_for_wc_refund() { + $parent_id = $this->create_multi_vendor_order(); + + $refund = $this->create_refund( $parent_id ); + + $this->assertEquals( $this->sut::DOKAN_PARENT_ORDER_REFUND, $this->sut->get_type( $refund ) ); + } + + public function test_order_type_for_dokan_single_order_refund() { + $this->seller_id2 = $this->seller_id1; + + $parent_id = $this->create_multi_vendor_order(); // Create single vendor order. + + $refund = $this->create_refund( $parent_id ); + + $this->assertEquals( $this->sut::DOKAN_SINGLE_ORDER_REFUND, $this->sut->get_type( $refund ) ); + } +} diff --git a/tests/php/src/Analytics/Reports/ProductQueryFilterTest.php b/tests/php/src/Analytics/Reports/ProductQueryFilterTest.php new file mode 100644 index 0000000000..cf1306e2ca --- /dev/null +++ b/tests/php/src/Analytics/Reports/ProductQueryFilterTest.php @@ -0,0 +1,173 @@ +sut = dokan_get_container()->get( QueryFilter::class ); + } + + protected function tearDown(): void { + parent::tearDown(); + + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $this->sut ); + } + /** + * Test that the order statistics hooks are registered correctly. + * + * @see https://giuseppe-mazzapica.gitbook.io/brain-monkey/wordpress-specific-tools/wordpress-hooks-added + * + * @return void + */ + public function test_products_hook_registered() { + $order_stats_query_filter = dokan_get_container()->get( QueryFilter::class ); + // Assert the Join Clause filters are registered + self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_join_products_subquery', [ $order_stats_query_filter, 'add_join_subquery' ] ) ); + // Assert the Where Clause filters are registered + self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_where_products_subquery', [ $order_stats_query_filter, 'add_where_subquery' ] ) ); + } + + /** + * Test dokan_order_stats JOIN and WHERE clauses are applied on order_stats select Query. + * + * Method(partial) mocking @see http://docs.mockery.io/en/latest/reference/partial_mocks.html + * + * @param int $order_id The order ID. + * @param int $vendor_id1 The first seller ID. + * @param int $vendor_id2 The second seller ID. + * @return void + */ + public function test_filter_hooks_are_applied_for_products_query() { + $order_id = $this->create_multi_vendor_order(); + + $this->run_all_pending(); + + $mocking_methods = [ + 'add_join_subquery', + 'add_where_subquery', + ]; + + $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' ); + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service ); + + foreach ( $mocking_methods as $method ) { + $service->shouldReceive( $method ) + ->atLeast() + ->once() + ->andReturnUsing( + function ( $clauses ) { + return $clauses; + } + ); + } + + $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\GenericQuery( [], 'products' ); + + $wc_stats_query->get_data(); + } + + /** + * + * @return void + */ + public function test_dokan_products_fields_are_selected_for_seller() { + $order_id = $this->create_multi_vendor_order(); + + $this->run_all_pending(); + + $mocking_methods = [ + 'should_filter_by_vendor_id', + ]; + + $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' ); + + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service ); + + remove_filter( 'woocommerce_analytics_clauses_where_products_subquery', [ $this->sut, 'add_where_subquery' ], 30 ); + + $service->shouldReceive( 'should_filter_by_vendor_id' ) + ->andReturnTrue(); + + $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\GenericQuery( [], 'products' ); + + $data = $wc_stats_query->get_data(); + + $sub_ids = dokan_get_suborder_ids_by( $order_id ); + + $report_data = $data->data; + + $this->assertCount( 2, $report_data ); + + // Assert that sub order items are fetched. + foreach ( $sub_ids as $s_id ) { + $s_order = wc_get_order( $s_id ); + + foreach ( $s_order->get_items() as $item ) { + $this->assertNestedContains( + [ + 'product_id' => $item->get_product_id(), + 'net_revenue' => floatval( $item->get_total() ), + 'items_sold' => $item->get_quantity(), + 'orders_count' => 1, + ], $report_data + ); + } + } + } + + public function test_dokan_products_fields_are_selected_for_admin() { + $order_id = $this->create_multi_vendor_order(); + + $this->run_all_pending(); + + $mocking_methods = [ + 'should_filter_by_vendor_id', + ]; + + $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' ); + + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service ); + + remove_filter( 'woocommerce_analytics_clauses_where_products_subquery', [ $this->sut, 'add_where_subquery' ], 30 ); + + $service->shouldReceive( 'should_filter_by_vendor_id' ) + ->andReturnFalse(); + + $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\GenericQuery( [], 'products' ); + + $data = $wc_stats_query->get_data(); + + $report_data = $data->data; + + $this->assertCount( 2, $report_data ); + + // Assert that parent order items are fetched. + $s_order = wc_get_order( $order_id ); + + foreach ( $s_order->get_items() as $item ) { + $this->assertNestedContains( + [ + 'product_id' => $item->get_product_id(), + 'net_revenue' => floatval( $item->get_total() ), + 'items_sold' => $item->get_quantity(), + 'orders_count' => 1, + ], $report_data + ); + } + } +} diff --git a/tests/php/src/Analytics/Reports/ProductStatsQueryFilterTest.php b/tests/php/src/Analytics/Reports/ProductStatsQueryFilterTest.php new file mode 100644 index 0000000000..f560212021 --- /dev/null +++ b/tests/php/src/Analytics/Reports/ProductStatsQueryFilterTest.php @@ -0,0 +1,169 @@ +sut = dokan_get_container()->get( QueryFilter::class ); + } + + protected function tearDown(): void { + parent::tearDown(); + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $this->sut ); + } + + /** + * Test that the order statistics hooks are registered correctly. + * + * @see https://giuseppe-mazzapica.gitbook.io/brain-monkey/wordpress-specific-tools/wordpress-hooks-added + * + * @return void + */ + public function test_products_stats_hook_registered() { + $order_stats_query_filter = dokan_get_container()->get( QueryFilter::class ); + // Assert the Join Clause filters are registered + self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_join_products_stats_total', [ $order_stats_query_filter, 'add_join_subquery' ] ) ); + self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_join_products_stats_interval', [ $order_stats_query_filter, 'add_join_subquery' ] ) ); + // Assert the Where Clause filters are registered + self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_where_products_stats_total', [ $order_stats_query_filter, 'add_where_subquery' ] ) ); + self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_where_products_stats_interval', [ $order_stats_query_filter, 'add_where_subquery' ] ) ); + } + + /** + * Test dokan_order_stats JOIN and WHERE clauses are applied on order_stats select Query. + * + * Method(partial) mocking @see http://docs.mockery.io/en/latest/reference/partial_mocks.html + * + * @return void + */ + public function test_dokan_products_states_query_filter_hooks_are_order_stats_update() { + $order_id = $this->create_multi_vendor_order(); + + $this->run_all_pending(); + + $mocking_methods = [ + 'add_join_subquery', + 'add_where_subquery', + ]; + + $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' ); + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service ); + + foreach ( $mocking_methods as $method ) { + $service->shouldReceive( $method ) + ->atLeast() + ->once() + ->andReturnUsing( + function ( $clauses ) { + return $clauses; + } + ); + } + + $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\GenericQuery( [], 'products-stats' ); + + $wc_stats_query->get_data(); + } + + /** + * + * @return void + */ + public function test_dokan_products_stats_added_to_wc_select_query_for_seller() { + $parent_id = $this->create_multi_vendor_order(); + + $this->run_all_pending(); + + $filter = Mockery::mock( QueryFilter::class . '[should_filter_by_vendor_id]' ); + + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $filter ); + + $filter->shouldReceive( 'should_filter_by_vendor_id' ) + ->atLeast() + ->once() + ->andReturnTrue(); + + $orders_query = new \Automattic\WooCommerce\Admin\API\Reports\GenericQuery( [], 'products-stats' ); + + $report_data = $orders_query->get_data(); + + $sub_ids = dokan_get_suborder_ids_by( $parent_id ); + + $this->assertCount( $report_data->totals->orders_count, $sub_ids ); + + $order = wc_get_order( $parent_id ); + + // Initialize a variable to hold the total + $line_items_total = 0; + + // Loop through each line item in the order + foreach ( $order->get_items() as $item_id => $item ) { + // Add the line item total to the cumulative total + $line_items_total += $item->get_total(); + } + + $this->assertCount( $report_data->totals->orders_count, $sub_ids ); + $this->assertEquals( $order->get_item_count(), $report_data->totals->items_sold ); + $this->assertEquals( $line_items_total, $report_data->totals->net_revenue ); + } + + /** + * + * @return void + */ + public function test_dokan_products_stats_added_to_wc_select_query_for_admin() { + $parent_id = $this->create_multi_vendor_order(); + + $this->run_all_pending(); + + $filter = Mockery::mock( QueryFilter::class . '[should_filter_by_vendor_id]' ); + + remove_filter( 'woocommerce_analytics_clauses_where_products_stats_total', [ $this->sut, 'add_where_subquery' ], 30 ); + remove_filter( 'woocommerce_analytics_clauses_where_products_stats_total', [ $this->sut, 'add_where_subquery' ], 30 ); + + dokan_get_container()->extend( QueryFilter::class )->setConcrete( $filter ); + + $filter->shouldReceive( 'should_filter_by_vendor_id' ) + ->atLeast() + ->once() + ->andReturnFalse(); + + $orders_query = new \Automattic\WooCommerce\Admin\API\Reports\GenericQuery( [], 'products-stats' ); + + $report_data = $orders_query->get_data(); + + $sub_ids = dokan_get_suborder_ids_by( $parent_id ); + + $this->assertEquals( 1, $report_data->totals->orders_count ); + + $order = wc_get_order( $parent_id ); + + // Initialize a variable to hold the total + $line_items_total = 0; + + // Loop through each line item in the order + foreach ( $order->get_items() as $item_id => $item ) { + // Add the line item total to the cumulative total + $line_items_total += $item->get_total(); + } + + $this->assertEquals( $order->get_item_count(), $report_data->totals->items_sold ); + $this->assertEquals( $line_items_total, $report_data->totals->net_revenue ); + } +} diff --git a/tests/php/src/Analytics/Reports/ReportTestCase.php b/tests/php/src/Analytics/Reports/ReportTestCase.php new file mode 100644 index 0000000000..b632014b98 --- /dev/null +++ b/tests/php/src/Analytics/Reports/ReportTestCase.php @@ -0,0 +1,113 @@ +get_items(); + $product_id = ''; + $line_item = []; + $line_item_id = ''; + + foreach ( $order_items as $key => $item ) { + $line_item_id = $item->get_id(); + + $qty = $item->get_quantity() - 1; + $amount = $item->get_total(); + $product_id = $item->get_product_id(); + $line_item = array( + 'qty' => $qty, + 'refund_total' => $amount, + 'refund_tax' => array(), + ); + + break; + } + + // Create the refund object. + $refund = wc_create_refund( + array( + 'reason' => 'Testing Refund', + 'order_id' => $order_id, + 'line_items' => [ $line_item_id => $line_item ], + ) + ); + + if ( $parent_refund && $order->get_parent_id() ) { + $parent_id = $order->get_parent_id(); + $parent_order = wc_get_order( $parent_id ); + $order_items = $parent_order->get_items(); + foreach ( $order_items as $key => $item ) { + if ( $product_id === $item->get_product_id() ) { + $line_item_id = $item->get_id(); + break; + } + } + + // Create the parent order refund object. + $parent_refund = wc_create_refund( + array( + 'reason' => 'Testing Parent Refund', + 'order_id' => $parent_id, + 'line_items' => [ $line_item_id => $line_item ], + ) + ); + + if ( $return_parent_refund ) { + return $parent_refund; + } + } + + return $refund; + } + + protected function set_order_meta_for_dokan( $parent_id, array $data ) { + // Filter null when order is single vendor order. + $sub_order_ids = array_filter( (array) dokan_get_suborder_ids_by( $parent_id ) ); + + // Fill sub orders meta data. + foreach ( $sub_order_ids as $sub_id ) { + $sub_order = wc_get_order( $sub_id ); + + foreach ( $data as $key => $val ) { + $sub_order->add_meta_data( '_' . $key, $val, true ); + } + + $sub_order->save_meta_data(); + $sub_order->save(); + } + + // Fill parent order meta data + $order = wc_get_order( $parent_id ); + $count = count( $sub_order_ids ) ? count( $sub_order_ids ) : 1; + + foreach ( $data as $key => $val ) { + $order->add_meta_data( '_' . $key, $val * $count, true ); + } + + $order->save_meta_data(); + $order->save(); + } + + public static function get_dokan_stats_data() { + return [ + [ + [ + 'vendor_earning' => random_int( 5, 10 ), + 'vendor_gateway_fee' => random_int( 5, 10 ), + 'vendor_discount' => random_int( 5, 10 ), + 'admin_commission' => random_int( 5, 10 ), + 'admin_gateway_fee' => random_int( 5, 10 ), + 'admin_discount' => random_int( 5, 10 ), + 'admin_subsidy' => random_int( 5, 10 ), + ], + ], + ]; + } +} diff --git a/tests/php/src/Analytics/Reports/StockProductQueryFilterTest.php b/tests/php/src/Analytics/Reports/StockProductQueryFilterTest.php new file mode 100644 index 0000000000..3934256223 --- /dev/null +++ b/tests/php/src/Analytics/Reports/StockProductQueryFilterTest.php @@ -0,0 +1,54 @@ +factory()->product->set_seller_id( $this->seller_id1 ) + ->create_many( 5 ); + + $seller2_prod_ids = $this->factory()->product->set_seller_id( $this->seller_id2 ) + ->create_many( 5 ); + + wp_set_current_user( $this->admin_id ); + + $_GET['sellers'] = $this->seller_id1; + + $response = $this->get_request( 'stock', [ 'sellers' => $this->seller_id1 ] ); + + $data = $response->get_data(); + + foreach ( $data as $item ) { + $prod = wc_get_product( $item['id'] ); + } + + $this->assertCount( count( $seller1_prod_ids ), $data ); + } + + public function tests_stock_reports_are_fetched_without_vendor_filter() { + $seller1_prod_ids = $this->factory()->product->set_seller_id( $this->seller_id1 ) + ->create_many( 5 ); + + $seller2_prod_ids = $this->factory()->product->set_seller_id( $this->seller_id2 ) + ->create_many( 5 ); + + wp_set_current_user( $this->admin_id ); + + $response = $this->get_request( 'stock' ); + + $data = $response->get_data(); + + foreach ( $data as $item ) { + $prod = wc_get_product( $item['id'] ); + } + + $this->assertCount( count( $seller1_prod_ids ) + count( $seller2_prod_ids ), $data ); + } +} diff --git a/tests/php/src/Analytics/Reports/StockStatsQueryFilterTest.php b/tests/php/src/Analytics/Reports/StockStatsQueryFilterTest.php new file mode 100644 index 0000000000..29e34b7093 --- /dev/null +++ b/tests/php/src/Analytics/Reports/StockStatsQueryFilterTest.php @@ -0,0 +1,53 @@ +factory()->product + ->set_seller_id( $this->seller_id1 ) + ->create_many( 5 ); + + $seller2_prod_ids = $this->factory()->product + ->set_seller_id( $this->seller_id2 ) + ->create_many( 5 ); + + wp_set_current_user( $this->admin_id ); + + $query = new \Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\Query(); + + $data = $query->get_data(); + $total = count( $seller1_prod_ids ) + count( $seller2_prod_ids ); + + $this->assertEquals( $total, $data['instock'] ); + $this->assertEquals( $total, $data['products'] ); + } + + public function tests_stock_stats_report_by_vendor_filter() { + $seller1_prod_ids = $this->factory()->product + ->set_seller_id( $this->seller_id1 ) + ->create_many( 5 ); + + $seller2_prod_ids = $this->factory()->product + ->set_seller_id( $this->seller_id2 ) + ->create_many( 5 ); + + wp_set_current_user( $this->admin_id ); + + $_GET['sellers'] = $this->seller_id1; + + $query = new \Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\Query(); + + $data = $query->get_data(); + $total = count( $seller1_prod_ids ); + + $this->assertEquals( $total, $data['instock'] ); + $this->assertEquals( $total, $data['products'] ); + } +} diff --git a/tests/php/src/DBAssertionTrait.php b/tests/php/src/DBAssertionTrait.php new file mode 100644 index 0000000000..34416b8a7d --- /dev/null +++ b/tests/php/src/DBAssertionTrait.php @@ -0,0 +1,77 @@ + 'val1', 'field1' => 'val1']; + // $placeholders = "field1='%s' AND field2='%s' "; + $placeholders = implode( + ' AND ', array_map( + function ( $key ) { + return "{$key} = %s"; + }, array_keys( $data ) + ) + ); + } else { + $data = [ 1 ]; + } + + if ( ! str_starts_with( $table, $wpdb->prefix ) ) { + $table = $wpdb->prefix . $table; + } + + $sql = $wpdb->prepare( + "SELECT COUNT(*) FROM $table WHERE $placeholders ", + array_values( $data ) + ); + + $rows_count = $wpdb->get_var( $sql ); + + return $rows_count; + } + + /** + * Assert that a table contains at least one row matching the specified criteria. + * + * @param string $table The name of the table (without the prefix). + * @param array $data An associative array of field-value pairs to match. + * @return void + */ + public function assertDatabaseHas( string $table, array $data = [] ): void { + $rows_count = $this->getDatabaseCount( $table, $data ); + + $this->assertGreaterThanOrEqual( 1, $rows_count, "No rows found in `$table` for given data " . json_encode( $data ) ); + } + + /** + * Assert that a table contains the specified number of rows matching the criteria. + * + * @param string $table The name of the table (without the prefix). + * @param int $count The expected number of matching rows. + * @param array $data An associative array of field-value pairs to match. + * @return void + */ + public function assertDatabaseCount( string $table, int $count, array $data = [] ): void { + $rows_count = $this->getDatabaseCount( $table, $data ); + + $this->assertEquals( $count, $rows_count, "No rows found in `$table` for given data " . json_encode( $data ) ); + } +} From 0c21fa6001e69a5bd432b91f9cf925b9ac9b12d2 Mon Sep 17 00:00:00 2001 From: "Md. Asif Hossain Nadim" <90011088+MdAsifHossainNadim@users.noreply.github.com> Date: Mon, 28 Oct 2024 13:37:46 +0600 Subject: [PATCH 07/12] enhance: Hide revenue coupon info from woocommerce analytics report. (#2418) --- assets/src/js/dokan-admin-analytics.js | 6 ++++++ includes/Assets.php | 10 +++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 assets/src/js/dokan-admin-analytics.js diff --git a/assets/src/js/dokan-admin-analytics.js b/assets/src/js/dokan-admin-analytics.js new file mode 100644 index 0000000000..b3ae8c54e9 --- /dev/null +++ b/assets/src/js/dokan-admin-analytics.js @@ -0,0 +1,6 @@ +// TODO: Remove this file after introducing Dokan coupon distribution. +wp.hooks.addFilter( + 'woocommerce_admin_revenue_report_charts', + 'dokan/remove-woocommerce-revenue-coupon-data-from/callback', + ( data ) => data.filter( item => item.key !== 'coupons' ) +); diff --git a/includes/Assets.php b/includes/Assets.php index 0f10b44320..8336437d50 100644 --- a/includes/Assets.php +++ b/includes/Assets.php @@ -129,6 +129,9 @@ public function enqueue_admin_scripts( $hook ) { wp_localize_script( 'dokan-admin-product', 'dokan_admin_product', $this->admin_product_localize_scripts() ); } + // Load admin scripts for analytics. + wp_enqueue_script( 'dokan-admin-analytics' ); + do_action( 'dokan_enqueue_admin_scripts', $hook ); } @@ -348,7 +351,7 @@ public function get_styles() { 'src' => DOKAN_PLUGIN_ASSEST . '/css/dokan-admin-product-style.css', 'version' => filemtime( DOKAN_DIR . '/assets/css/dokan-admin-product-style.css' ), ], - 'dokan-tailwind' => [ + 'dokan-tailwind' => [ 'src' => DOKAN_PLUGIN_ASSEST . '/css/dokan-tailwind.css', 'version' => filemtime( DOKAN_DIR . '/assets/css/dokan-tailwind.css' ), ], @@ -554,6 +557,11 @@ public function get_scripts() { 'deps' => [ 'jquery' ], 'version' => filemtime( $asset_path . 'js/dokan-frontend.js' ), ], + 'dokan-admin-analytics' => [ + 'src' => $asset_url . '/src/js/dokan-admin-analytics.js', + 'deps' => [ 'wc-admin-app', 'wp-hooks' ], + 'version' => filemtime( $asset_path . 'src/js/dokan-admin-analytics.js' ), + ], ]; return $scripts; From fe8828de8163675cf03afd9e5ec41d9a849e9a6c Mon Sep 17 00:00:00 2001 From: Md Mahbub Rabbani Date: Mon, 28 Oct 2024 14:13:43 +0600 Subject: [PATCH 08/12] Remove unused category stats --- .../Reports/Categories/Stats/QueryFilter.php | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 includes/Analytics/Reports/Categories/Stats/QueryFilter.php diff --git a/includes/Analytics/Reports/Categories/Stats/QueryFilter.php b/includes/Analytics/Reports/Categories/Stats/QueryFilter.php deleted file mode 100644 index f9a75efadd..0000000000 --- a/includes/Analytics/Reports/Categories/Stats/QueryFilter.php +++ /dev/null @@ -1,22 +0,0 @@ - Date: Mon, 28 Oct 2024 19:15:52 +0600 Subject: [PATCH 09/12] Fix/add dokan since (#2420) * Add since version * Add Dokan Since in docblock * Fix PHPCS issue --- dokan-class.php | 7 +++++++ dokan.php | 9 ++++++++- includes/Analytics/Reports/BaseQueryFilter.php | 2 ++ .../Analytics/Reports/Categories/QueryFilter.php | 5 +++-- includes/Analytics/Reports/Coupons/QueryFilter.php | 2 ++ .../Reports/Coupons/Stats/QueryFilter.php | 3 +-- .../Reports/Coupons/Stats/WcDataStore.php | 5 +++++ .../Analytics/Reports/Customers/QueryFilter.php | 2 ++ .../Reports/Customers/Stats/QueryFilter.php | 5 +++++ includes/Analytics/Reports/DataStoreModifier.php | 2 ++ includes/Analytics/Reports/OrderType.php | 2 +- includes/Analytics/Reports/Orders/QueryFilter.php | 2 ++ .../Analytics/Reports/Orders/Stats/DataStore.php | 8 +++----- .../Analytics/Reports/Orders/Stats/QueryFilter.php | 2 +- .../Reports/Orders/Stats/ScheduleListener.php | 2 ++ .../Analytics/Reports/Orders/Stats/WcDataStore.php | 5 +++++ .../Analytics/Reports/Products/QueryFilter.php | 5 +++-- .../Reports/Products/Stats/QueryFilter.php | 5 +++++ .../Reports/Products/Stats/WcDataStore.php | 5 +++++ includes/Analytics/Reports/Stock/QueryFilter.php | 7 +++++++ .../Analytics/Reports/Stock/Stats/WcDataStore.php | 11 +++++++++++ includes/Analytics/Reports/Taxes/QueryFilter.php | 4 +++- .../Analytics/Reports/Taxes/Stats/QueryFilter.php | 7 +++++++ .../Analytics/Reports/Taxes/Stats/WcDataStore.php | 5 +++++ .../Analytics/Reports/Variations/QueryFilter.php | 4 +++- .../Reports/Variations/Stats/QueryFilter.php | 7 +++++++ includes/Analytics/Reports/WcSqlQuery.php | 5 +++++ .../DependencyManagement/BaseServiceProvider.php | 2 ++ .../BootableServiceProvider.php | 2 ++ includes/DependencyManagement/Container.php | 4 +++- .../DependencyManagement/ContainerException.php | 2 ++ .../Providers/AdminServiceProvider.php | 3 +++ .../Providers/AjaxServiceProvider.php | 3 +++ .../Providers/AnalyticsServiceProvider.php | 3 +++ .../Providers/CommonServiceProvider.php | 3 +++ .../Providers/FrontendServiceProvider.php | 3 +++ .../Providers/ServiceProvider.php | 14 +++++++++++++- 37 files changed, 149 insertions(+), 18 deletions(-) diff --git a/dokan-class.php b/dokan-class.php index 12392395ad..1bba87b2fa 100755 --- a/dokan-class.php +++ b/dokan-class.php @@ -476,6 +476,13 @@ public function get_db_version_key() { return $this->db_version_key; } + /** + * Retrieve the container instance. + * + * @since DOKAN_SINCE + * + * @return Container + */ public function get_container(): Container { return dokan_get_container(); } diff --git a/dokan.php b/dokan.php index 4aa8a5f0d8..e970bfa08e 100755 --- a/dokan.php +++ b/dokan.php @@ -46,7 +46,12 @@ } require_once __DIR__ . '/vendor/autoload.php'; -// Load files for loading the WeDevs_Dokan class. + +/** + * Include file for loading the WeDevs_Dokan class. + * + * @since DOKAN_SINCE + */ require_once __DIR__ . '/dokan-class.php'; // Define constant for the Plugin file. @@ -67,6 +72,8 @@ /** * Get the container. * + * @since DOKAN_SINCE + * * @return Container The global container instance. */ function dokan_get_container(): Container { diff --git a/includes/Analytics/Reports/BaseQueryFilter.php b/includes/Analytics/Reports/BaseQueryFilter.php index 72e92968e1..a9ad0e1751 100644 --- a/includes/Analytics/Reports/BaseQueryFilter.php +++ b/includes/Analytics/Reports/BaseQueryFilter.php @@ -10,6 +10,8 @@ * Class QueryFilter * * Filters and modifies WooCommerce analytics queries for Dokan orders. + * + * @since DOKAN_SINCE */ abstract class BaseQueryFilter implements Hookable { protected $wc_table = 'wc_order_stats'; diff --git a/includes/Analytics/Reports/Categories/QueryFilter.php b/includes/Analytics/Reports/Categories/QueryFilter.php index d5428315b9..269767e383 100644 --- a/includes/Analytics/Reports/Categories/QueryFilter.php +++ b/includes/Analytics/Reports/Categories/QueryFilter.php @@ -7,7 +7,9 @@ /** * Class QueryFilter * - * Filters and modifies WooCommerce analytics queries for Dokan orders. + * Filters and modifies WooCommerce analytics queries for Categories. + * + * @since DOKAN_SINCE */ class QueryFilter extends BaseQueryFilter { protected $wc_table = 'wc_order_product_lookup'; @@ -23,7 +25,6 @@ class QueryFilter extends BaseQueryFilter { * @return void */ public function register_hooks(): void { - //woocommerce_analytics_clauses_ // add_filter( 'woocommerce_analytics_clauses_join_products', [ $this, 'add_join_subquery' ] ); add_filter( 'woocommerce_analytics_clauses_join_categories_subquery', [ $this, 'add_join_subquery' ] ); add_filter( 'woocommerce_analytics_clauses_where_categories_subquery', [ $this, 'add_where_subquery' ], 30 ); diff --git a/includes/Analytics/Reports/Coupons/QueryFilter.php b/includes/Analytics/Reports/Coupons/QueryFilter.php index 0c84fb71cf..91dccaf043 100644 --- a/includes/Analytics/Reports/Coupons/QueryFilter.php +++ b/includes/Analytics/Reports/Coupons/QueryFilter.php @@ -9,6 +9,8 @@ * Class QueryFilter * * Filters and modifies WooCommerce analytics queries for Dokan orders. + * + * @since DOKAN_SINCE */ class QueryFilter extends BaseQueryFilter { /** diff --git a/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php b/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php index 9c1e33c560..8bef1b03f3 100644 --- a/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php +++ b/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php @@ -3,7 +3,6 @@ namespace WeDevs\Dokan\Analytics\Reports\Coupons\Stats; use WeDevs\Dokan\Analytics\Reports\Coupons\QueryFilter as OrdersQueryFilter; -use WeDevs\Dokan\Analytics\Reports\OrderType; /** * Class QueryFilter @@ -11,7 +10,7 @@ * Extends the OrdersQueryFilter class to customize WooCommerce Analytics reports * for Dokan orders stats by adding additional subqueries and modifying report columns. * - * @package WeDevs\Dokan\Analytics\Reports\Orders\Stats + * @since DOKAN_SINCE */ class QueryFilter extends OrdersQueryFilter { /** diff --git a/includes/Analytics/Reports/Coupons/Stats/WcDataStore.php b/includes/Analytics/Reports/Coupons/Stats/WcDataStore.php index 4fb6f04aa2..d0b80ae120 100644 --- a/includes/Analytics/Reports/Coupons/Stats/WcDataStore.php +++ b/includes/Analytics/Reports/Coupons/Stats/WcDataStore.php @@ -5,6 +5,11 @@ use Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\DataStore as OrdersStateDataStore; use WeDevs\Dokan\Analytics\Reports\WcSqlQuery; +/** + * WC DataStore class to override the default handling of SQL clauses. + * + * @since DOKAN_SINCE + */ class WcDataStore extends OrdersStateDataStore { /** * Override the $total_query and $interval_query properties to customize query behavior. diff --git a/includes/Analytics/Reports/Customers/QueryFilter.php b/includes/Analytics/Reports/Customers/QueryFilter.php index 7fcdf72694..d0c710b568 100644 --- a/includes/Analytics/Reports/Customers/QueryFilter.php +++ b/includes/Analytics/Reports/Customers/QueryFilter.php @@ -8,6 +8,8 @@ * Class QueryFilter * * Filters and modifies WooCommerce analytics queries for Dokan orders. + * + * @since DOKAN_SINCE */ class QueryFilter extends BaseQueryFilter { /** diff --git a/includes/Analytics/Reports/Customers/Stats/QueryFilter.php b/includes/Analytics/Reports/Customers/Stats/QueryFilter.php index 3a10e7b1ca..4b080e0922 100644 --- a/includes/Analytics/Reports/Customers/Stats/QueryFilter.php +++ b/includes/Analytics/Reports/Customers/Stats/QueryFilter.php @@ -4,6 +4,11 @@ use WeDevs\Dokan\Analytics\Reports\Customers\QueryFilter as CustomersQueryFilter; +/** + * Class QueryFilter + * + * @since DOKAN_SINCE + */ class QueryFilter extends CustomersQueryFilter { protected $context = 'customers_stats'; diff --git a/includes/Analytics/Reports/DataStoreModifier.php b/includes/Analytics/Reports/DataStoreModifier.php index 2048614054..97d6168bf8 100644 --- a/includes/Analytics/Reports/DataStoreModifier.php +++ b/includes/Analytics/Reports/DataStoreModifier.php @@ -6,6 +6,8 @@ /** * WC default data store modifier. + * + * @since DOKAN_SINCE */ class DataStoreModifier implements Hookable { /** diff --git a/includes/Analytics/Reports/OrderType.php b/includes/Analytics/Reports/OrderType.php index 47b4691b16..027eb70255 100644 --- a/includes/Analytics/Reports/OrderType.php +++ b/includes/Analytics/Reports/OrderType.php @@ -9,7 +9,7 @@ * * Defines constants and methods to handle different types of Dokan orders and refunds. * - * @package WeDevs\Dokan\Analytics\Reports + * @since DOKAN_SINCE */ class OrderType { // Order type constants diff --git a/includes/Analytics/Reports/Orders/QueryFilter.php b/includes/Analytics/Reports/Orders/QueryFilter.php index 85d85d3944..4160eb9f45 100644 --- a/includes/Analytics/Reports/Orders/QueryFilter.php +++ b/includes/Analytics/Reports/Orders/QueryFilter.php @@ -9,6 +9,8 @@ * Class QueryFilter * * Filters and modifies WooCommerce analytics queries for Dokan orders. + * + * @since DOKAN_SINCE */ class QueryFilter extends BaseQueryFilter { /** diff --git a/includes/Analytics/Reports/Orders/Stats/DataStore.php b/includes/Analytics/Reports/Orders/Stats/DataStore.php index 6428c3680a..6fa523bfc4 100644 --- a/includes/Analytics/Reports/Orders/Stats/DataStore.php +++ b/includes/Analytics/Reports/Orders/Stats/DataStore.php @@ -1,7 +1,4 @@ \WeDevs\Dokan\Blocks\ProductBlock::class, From 2cffa360a94b32033e7591fece5950068ab758f5 Mon Sep 17 00:00:00 2001 From: "Md. Asif Hossain Nadim" <90011088+MdAsifHossainNadim@users.noreply.github.com> Date: Mon, 28 Oct 2024 19:16:41 +0600 Subject: [PATCH 10/12] fix: Dokan dmin analytics script build stuff. (#2419) --- includes/Assets.php | 4 ++-- webpack.config.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/Assets.php b/includes/Assets.php index 8336437d50..e39d4d8a1f 100644 --- a/includes/Assets.php +++ b/includes/Assets.php @@ -558,9 +558,9 @@ public function get_scripts() { 'version' => filemtime( $asset_path . 'js/dokan-frontend.js' ), ], 'dokan-admin-analytics' => [ - 'src' => $asset_url . '/src/js/dokan-admin-analytics.js', + 'src' => $asset_url . '/js/dokan-admin-analytics.js', 'deps' => [ 'wc-admin-app', 'wp-hooks' ], - 'version' => filemtime( $asset_path . 'src/js/dokan-admin-analytics.js' ), + 'version' => filemtime( $asset_path . 'js/dokan-admin-analytics.js' ), ], ]; diff --git a/webpack.config.js b/webpack.config.js index d0c8433caa..3517e4d421 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -40,6 +40,7 @@ const entryPoint = { ], 'helper': './assets/src/js/helper.js', 'dokan-frontend': './assets/src/js/dokan-frontend.js', + 'dokan-admin-analytics': './assets/src/js/dokan-admin-analytics.js', 'style': '/assets/src/less/style.less', 'rtl': '/assets/src/less/rtl.less', From 491ce93ac0e89c3c6785a7dcc16c3cbfb708d2d0 Mon Sep 17 00:00:00 2001 From: Shazahanul Islam Shohag Date: Mon, 28 Oct 2024 21:03:17 +0600 Subject: [PATCH 11/12] chore: Release Version 3.13.0 --- README.md | 7 +- appsero.json | 7 +- assets/js/dokan-admin-analytics.asset.php | 1 + assets/js/dokan-admin-analytics.js | 1 + bin/version-replace.js | 1 + bin/zip.js | 1 + dokan-class.php | 4 +- dokan.php | 6 +- .../Analytics/Reports/BaseQueryFilter.php | 2 +- .../Reports/Categories/QueryFilter.php | 2 +- .../Analytics/Reports/Coupons/QueryFilter.php | 2 +- .../Reports/Coupons/Stats/QueryFilter.php | 2 +- .../Reports/Coupons/Stats/WcDataStore.php | 2 +- .../Reports/Customers/QueryFilter.php | 2 +- .../Reports/Customers/Stats/QueryFilter.php | 2 +- .../Analytics/Reports/DataStoreModifier.php | 2 +- includes/Analytics/Reports/OrderType.php | 2 +- .../Analytics/Reports/Orders/QueryFilter.php | 2 +- .../Reports/Orders/Stats/DataStore.php | 8 +- .../Reports/Orders/Stats/QueryFilter.php | 2 +- .../Reports/Orders/Stats/ScheduleListener.php | 2 +- .../Reports/Orders/Stats/WcDataStore.php | 2 +- .../Reports/Products/QueryFilter.php | 2 +- .../Reports/Products/Stats/QueryFilter.php | 2 +- .../Reports/Products/Stats/WcDataStore.php | 2 +- .../Analytics/Reports/Stock/QueryFilter.php | 2 +- .../Reports/Stock/Stats/WcDataStore.php | 2 +- .../Analytics/Reports/Taxes/QueryFilter.php | 2 +- .../Reports/Taxes/Stats/QueryFilter.php | 2 +- .../Reports/Taxes/Stats/WcDataStore.php | 2 +- .../Reports/Variations/QueryFilter.php | 2 +- .../Reports/Variations/Stats/QueryFilter.php | 2 +- includes/Analytics/Reports/WcSqlQuery.php | 2 +- .../BaseServiceProvider.php | 2 +- .../BootableServiceProvider.php | 2 +- includes/DependencyManagement/Container.php | 2 +- .../ContainerException.php | 2 +- .../Providers/ServiceProvider.php | 2 +- languages/dokan-lite.pot | 301 +++++++++--------- package-lock.json | 4 +- package.json | 2 +- readme.txt | 7 +- templates/whats-new.php | 16 + .../Reports/StockStatsQueryFilterTest.php | 2 +- 44 files changed, 225 insertions(+), 201 deletions(-) create mode 100644 assets/js/dokan-admin-analytics.asset.php create mode 100644 assets/js/dokan-admin-analytics.js diff --git a/README.md b/README.md index 14e2b3194c..35feab89dc 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ **WC requires at least:** 8.0.0 **WC tested up to:** 9.3.3 **Requires PHP:** 7.4 -**Stable tag:** 3.12.6 +**Stable tag:** 3.13.0 **License:** GPLv2 or later **License URI:** http://www.gnu.org/licenses/gpl-2.0.html @@ -347,6 +347,11 @@ A. Just install and activate the PRO version without deleting the free plugin. A ## Changelog ## +### v3.13.0 ( Oct 28, 2024 ) ### + +- **feat:** Replaced the Dokan array container with the League Container, ensuring backward compatibility for seamless performance and enhanced flexibility. +- **feat:** Updated Dokan to be fully compatible with WooCommerce Analytics Reports + ### v3.12.6 ( Oct 24, 2024 ) ### - **fix:** Fixed js error on frontend pages. diff --git a/appsero.json b/appsero.json index 011b247974..4ccf068ba6 100644 --- a/appsero.json +++ b/appsero.json @@ -31,6 +31,11 @@ "package.json", ".php_cs", ".babelrc", - ".editorconfig" + ".editorconfig", + ".eslintrc", + "postcss.config.js", + "webpack.config.js", + "tailwind.config.js", + ".prettierrc.js" ] } diff --git a/assets/js/dokan-admin-analytics.asset.php b/assets/js/dokan-admin-analytics.asset.php new file mode 100644 index 0000000000..c7b380e547 --- /dev/null +++ b/assets/js/dokan-admin-analytics.asset.php @@ -0,0 +1 @@ + array(), 'version' => '33fd710a64e86a40d022'); diff --git a/assets/js/dokan-admin-analytics.js b/assets/js/dokan-admin-analytics.js new file mode 100644 index 0000000000..f4e3c812d2 --- /dev/null +++ b/assets/js/dokan-admin-analytics.js @@ -0,0 +1 @@ +wp.hooks.addFilter("woocommerce_admin_revenue_report_charts","dokan/remove-woocommerce-revenue-coupon-data-from/callback",(e=>e.filter((e=>"coupons"!==e.key)))); \ No newline at end of file diff --git a/bin/version-replace.js b/bin/version-replace.js index 41dafc8ac8..0550986012 100644 --- a/bin/version-replace.js +++ b/bin/version-replace.js @@ -8,6 +8,7 @@ const pluginFiles = [ 'templates/**/*', 'src/**/*', 'dokan.php', + 'dokan-class.php', 'uninstall.php', ]; diff --git a/bin/zip.js b/bin/zip.js index 6397080e7c..e589689161 100644 --- a/bin/zip.js +++ b/bin/zip.js @@ -15,6 +15,7 @@ const pluginFiles = [ 'CHANGELOG.md', 'readme.txt', 'dokan.php', + 'dokan-class.php', 'uninstall.php', 'composer.json', ]; diff --git a/dokan-class.php b/dokan-class.php index 1bba87b2fa..6c960286c4 100755 --- a/dokan-class.php +++ b/dokan-class.php @@ -23,7 +23,7 @@ final class WeDevs_Dokan { * * @var string */ - public $version = '3.11.3'; + public $version = '3.13.0'; /** * Instance of self @@ -479,7 +479,7 @@ public function get_db_version_key() { /** * Retrieve the container instance. * - * @since DOKAN_SINCE + * @since 3.13.0 * * @return Container */ diff --git a/dokan.php b/dokan.php index e970bfa08e..b3482c3574 100755 --- a/dokan.php +++ b/dokan.php @@ -3,7 +3,7 @@ * Plugin Name: Dokan * Plugin URI: https://dokan.co/wordpress/ * Description: An e-commerce marketplace plugin for WordPress. Powered by WooCommerce and weDevs. - * Version: 3.12.6 + * Version: 3.13.0 * Author: weDevs * Author URI: https://dokan.co/ * Text Domain: dokan-lite @@ -50,7 +50,7 @@ /** * Include file for loading the WeDevs_Dokan class. * - * @since DOKAN_SINCE + * @since 3.13.0 */ require_once __DIR__ . '/dokan-class.php'; @@ -72,7 +72,7 @@ /** * Get the container. * - * @since DOKAN_SINCE + * @since 3.13.0 * * @return Container The global container instance. */ diff --git a/includes/Analytics/Reports/BaseQueryFilter.php b/includes/Analytics/Reports/BaseQueryFilter.php index a9ad0e1751..c5e4f58be4 100644 --- a/includes/Analytics/Reports/BaseQueryFilter.php +++ b/includes/Analytics/Reports/BaseQueryFilter.php @@ -11,7 +11,7 @@ * * Filters and modifies WooCommerce analytics queries for Dokan orders. * - * @since DOKAN_SINCE + * @since 3.13.0 */ abstract class BaseQueryFilter implements Hookable { protected $wc_table = 'wc_order_stats'; diff --git a/includes/Analytics/Reports/Categories/QueryFilter.php b/includes/Analytics/Reports/Categories/QueryFilter.php index 269767e383..2aed675880 100644 --- a/includes/Analytics/Reports/Categories/QueryFilter.php +++ b/includes/Analytics/Reports/Categories/QueryFilter.php @@ -9,7 +9,7 @@ * * Filters and modifies WooCommerce analytics queries for Categories. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends BaseQueryFilter { protected $wc_table = 'wc_order_product_lookup'; diff --git a/includes/Analytics/Reports/Coupons/QueryFilter.php b/includes/Analytics/Reports/Coupons/QueryFilter.php index 91dccaf043..990c1cc9c1 100644 --- a/includes/Analytics/Reports/Coupons/QueryFilter.php +++ b/includes/Analytics/Reports/Coupons/QueryFilter.php @@ -10,7 +10,7 @@ * * Filters and modifies WooCommerce analytics queries for Dokan orders. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends BaseQueryFilter { /** diff --git a/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php b/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php index 8bef1b03f3..03bebebd6d 100644 --- a/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php +++ b/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php @@ -10,7 +10,7 @@ * Extends the OrdersQueryFilter class to customize WooCommerce Analytics reports * for Dokan orders stats by adding additional subqueries and modifying report columns. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends OrdersQueryFilter { /** diff --git a/includes/Analytics/Reports/Coupons/Stats/WcDataStore.php b/includes/Analytics/Reports/Coupons/Stats/WcDataStore.php index d0b80ae120..8daf44ef96 100644 --- a/includes/Analytics/Reports/Coupons/Stats/WcDataStore.php +++ b/includes/Analytics/Reports/Coupons/Stats/WcDataStore.php @@ -8,7 +8,7 @@ /** * WC DataStore class to override the default handling of SQL clauses. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class WcDataStore extends OrdersStateDataStore { /** diff --git a/includes/Analytics/Reports/Customers/QueryFilter.php b/includes/Analytics/Reports/Customers/QueryFilter.php index d0c710b568..f03afe9cf5 100644 --- a/includes/Analytics/Reports/Customers/QueryFilter.php +++ b/includes/Analytics/Reports/Customers/QueryFilter.php @@ -9,7 +9,7 @@ * * Filters and modifies WooCommerce analytics queries for Dokan orders. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends BaseQueryFilter { /** diff --git a/includes/Analytics/Reports/Customers/Stats/QueryFilter.php b/includes/Analytics/Reports/Customers/Stats/QueryFilter.php index 4b080e0922..d59e3b9525 100644 --- a/includes/Analytics/Reports/Customers/Stats/QueryFilter.php +++ b/includes/Analytics/Reports/Customers/Stats/QueryFilter.php @@ -7,7 +7,7 @@ /** * Class QueryFilter * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends CustomersQueryFilter { protected $context = 'customers_stats'; diff --git a/includes/Analytics/Reports/DataStoreModifier.php b/includes/Analytics/Reports/DataStoreModifier.php index 97d6168bf8..a7171c8a69 100644 --- a/includes/Analytics/Reports/DataStoreModifier.php +++ b/includes/Analytics/Reports/DataStoreModifier.php @@ -7,7 +7,7 @@ /** * WC default data store modifier. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class DataStoreModifier implements Hookable { /** diff --git a/includes/Analytics/Reports/OrderType.php b/includes/Analytics/Reports/OrderType.php index 027eb70255..e396838ace 100644 --- a/includes/Analytics/Reports/OrderType.php +++ b/includes/Analytics/Reports/OrderType.php @@ -9,7 +9,7 @@ * * Defines constants and methods to handle different types of Dokan orders and refunds. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class OrderType { // Order type constants diff --git a/includes/Analytics/Reports/Orders/QueryFilter.php b/includes/Analytics/Reports/Orders/QueryFilter.php index 4160eb9f45..315b0fa72f 100644 --- a/includes/Analytics/Reports/Orders/QueryFilter.php +++ b/includes/Analytics/Reports/Orders/QueryFilter.php @@ -10,7 +10,7 @@ * * Filters and modifies WooCommerce analytics queries for Dokan orders. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends BaseQueryFilter { /** diff --git a/includes/Analytics/Reports/Orders/Stats/DataStore.php b/includes/Analytics/Reports/Orders/Stats/DataStore.php index 6fa523bfc4..7662b164e9 100644 --- a/includes/Analytics/Reports/Orders/Stats/DataStore.php +++ b/includes/Analytics/Reports/Orders/Stats/DataStore.php @@ -14,7 +14,7 @@ /** * Dokan Orders stats data synchronizer. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class DataStore extends ReportsDataStore implements DataStoreInterface { /** @@ -140,7 +140,7 @@ public static function update( $order ) { * @param array $data Data written to order stats lookup table. * @param WC_Order $order Order object. * - * @since DOKAN_SINCE + * @since 3.13.0 */ $data = apply_filters( 'dokan_analytics_update_order_stats_data', @@ -188,7 +188,7 @@ public static function update( $order ) { * * @param int $order_id Order ID. * - * @since DOKAN_SINCE + * @since 3.13.0 */ do_action( 'dokan_analytics_update_order_stats', $order->get_id() ); @@ -222,7 +222,7 @@ public static function delete_order( $post_id ) { * @param int $order_id Order ID. * @param int $customer_id Customer ID. * - * @since DOKAN_SINCE + * @since 3.13.0 */ do_action( 'dokan_analytics_delete_order_stats', $order_id, $customer_id ); } diff --git a/includes/Analytics/Reports/Orders/Stats/QueryFilter.php b/includes/Analytics/Reports/Orders/Stats/QueryFilter.php index 3a242d4b79..9253f92fb4 100644 --- a/includes/Analytics/Reports/Orders/Stats/QueryFilter.php +++ b/includes/Analytics/Reports/Orders/Stats/QueryFilter.php @@ -10,7 +10,7 @@ * Extends the OrdersQueryFilter class to customize WooCommerce Analytics reports * for Dokan orders stats by adding additional subqueries and modifying report columns. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends OrdersQueryFilter { /** diff --git a/includes/Analytics/Reports/Orders/Stats/ScheduleListener.php b/includes/Analytics/Reports/Orders/Stats/ScheduleListener.php index 0a87972638..645d60d41e 100644 --- a/includes/Analytics/Reports/Orders/Stats/ScheduleListener.php +++ b/includes/Analytics/Reports/Orders/Stats/ScheduleListener.php @@ -9,7 +9,7 @@ * * Listens to WooCommerce schedule events and triggers Dokan order synchronization and deletion. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class ScheduleListener implements Hookable { /** diff --git a/includes/Analytics/Reports/Orders/Stats/WcDataStore.php b/includes/Analytics/Reports/Orders/Stats/WcDataStore.php index a249be8aac..c410c65173 100644 --- a/includes/Analytics/Reports/Orders/Stats/WcDataStore.php +++ b/includes/Analytics/Reports/Orders/Stats/WcDataStore.php @@ -8,7 +8,7 @@ /** * DataStore class to override the default handling of WC SQL clauses. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class WcDataStore extends OrdersStateDataStore { /** diff --git a/includes/Analytics/Reports/Products/QueryFilter.php b/includes/Analytics/Reports/Products/QueryFilter.php index bac90ca7e3..c56062ae77 100644 --- a/includes/Analytics/Reports/Products/QueryFilter.php +++ b/includes/Analytics/Reports/Products/QueryFilter.php @@ -9,7 +9,7 @@ * * Filters and modifies WooCommerce analytics queries for Dokan Products. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends BaseQueryFilter { protected $wc_table = 'wc_order_product_lookup'; diff --git a/includes/Analytics/Reports/Products/Stats/QueryFilter.php b/includes/Analytics/Reports/Products/Stats/QueryFilter.php index a8ce4eee1c..9b32520f10 100644 --- a/includes/Analytics/Reports/Products/Stats/QueryFilter.php +++ b/includes/Analytics/Reports/Products/Stats/QueryFilter.php @@ -7,7 +7,7 @@ /** * Filters and modifies WooCommerce analytics queries for Dokan Products Stats. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends ProductsQueryFilter { protected $context = 'products_stats'; diff --git a/includes/Analytics/Reports/Products/Stats/WcDataStore.php b/includes/Analytics/Reports/Products/Stats/WcDataStore.php index 03dd9b45e9..48b875ca9d 100644 --- a/includes/Analytics/Reports/Products/Stats/WcDataStore.php +++ b/includes/Analytics/Reports/Products/Stats/WcDataStore.php @@ -8,7 +8,7 @@ /** * DataStore class to override the default handling of WC SQL clauses. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class WcDataStore extends ProductStatsDataStore { /** diff --git a/includes/Analytics/Reports/Stock/QueryFilter.php b/includes/Analytics/Reports/Stock/QueryFilter.php index 940f3d6940..1abee9f339 100644 --- a/includes/Analytics/Reports/Stock/QueryFilter.php +++ b/includes/Analytics/Reports/Stock/QueryFilter.php @@ -11,7 +11,7 @@ * * Filters and modifies WooCommerce analytics queries for Product Stock. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends BaseQueryFilter { protected $should_removed_where_filter = true; diff --git a/includes/Analytics/Reports/Stock/Stats/WcDataStore.php b/includes/Analytics/Reports/Stock/Stats/WcDataStore.php index b96224e1aa..4c7e359678 100644 --- a/includes/Analytics/Reports/Stock/Stats/WcDataStore.php +++ b/includes/Analytics/Reports/Stock/Stats/WcDataStore.php @@ -10,7 +10,7 @@ * * Filters and modifies WooCommerce analytics queries for Stock Stats. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class WcDataStore extends StockStatsDataStore { /** diff --git a/includes/Analytics/Reports/Taxes/QueryFilter.php b/includes/Analytics/Reports/Taxes/QueryFilter.php index efde648f4f..aa6b6d27c6 100644 --- a/includes/Analytics/Reports/Taxes/QueryFilter.php +++ b/includes/Analytics/Reports/Taxes/QueryFilter.php @@ -10,7 +10,7 @@ * * Filters and modifies WooCommerce analytics queries for Taxes. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends BaseQueryFilter { protected $wc_table = 'wc_order_tax_lookup'; diff --git a/includes/Analytics/Reports/Taxes/Stats/QueryFilter.php b/includes/Analytics/Reports/Taxes/Stats/QueryFilter.php index 1b99d4a3f1..53467b638d 100644 --- a/includes/Analytics/Reports/Taxes/Stats/QueryFilter.php +++ b/includes/Analytics/Reports/Taxes/Stats/QueryFilter.php @@ -9,7 +9,7 @@ * * Filters and modifies WooCommerce analytics queries for Tax Stats. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends TaxesQueryFilter { protected $context = 'taxes_stats'; diff --git a/includes/Analytics/Reports/Taxes/Stats/WcDataStore.php b/includes/Analytics/Reports/Taxes/Stats/WcDataStore.php index 23b53c60a2..a76a5d052a 100644 --- a/includes/Analytics/Reports/Taxes/Stats/WcDataStore.php +++ b/includes/Analytics/Reports/Taxes/Stats/WcDataStore.php @@ -8,7 +8,7 @@ /** * WC DataStore class to override the default handling of SQL clauses. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class WcDataStore extends TaxesStateDataStore { /** diff --git a/includes/Analytics/Reports/Variations/QueryFilter.php b/includes/Analytics/Reports/Variations/QueryFilter.php index 91a2e76fd7..d17f32b94b 100644 --- a/includes/Analytics/Reports/Variations/QueryFilter.php +++ b/includes/Analytics/Reports/Variations/QueryFilter.php @@ -10,7 +10,7 @@ * * Filters and modifies WooCommerce analytics queries for variations. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends BaseQueryFilter { protected $wc_table = 'wc_order_product_lookup'; diff --git a/includes/Analytics/Reports/Variations/Stats/QueryFilter.php b/includes/Analytics/Reports/Variations/Stats/QueryFilter.php index 9b7d058b40..b2d24cd660 100644 --- a/includes/Analytics/Reports/Variations/Stats/QueryFilter.php +++ b/includes/Analytics/Reports/Variations/Stats/QueryFilter.php @@ -9,7 +9,7 @@ * * Filters and modifies WooCommerce analytics queries for variations. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class QueryFilter extends ProductsQueryFilter { protected $context = 'variations_stats'; diff --git a/includes/Analytics/Reports/WcSqlQuery.php b/includes/Analytics/Reports/WcSqlQuery.php index 3d5b2fd4e1..1289ae7628 100644 --- a/includes/Analytics/Reports/WcSqlQuery.php +++ b/includes/Analytics/Reports/WcSqlQuery.php @@ -7,7 +7,7 @@ /** * WC SqlQuery class to override the default handling of SQL clauses. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class WcSqlQuery extends SqlQuery { /** diff --git a/includes/DependencyManagement/BaseServiceProvider.php b/includes/DependencyManagement/BaseServiceProvider.php index e8b1c6e4e9..0ad0ae078e 100644 --- a/includes/DependencyManagement/BaseServiceProvider.php +++ b/includes/DependencyManagement/BaseServiceProvider.php @@ -16,7 +16,7 @@ * Note that `AbstractInterfaceServiceProvider` likely serves as a better base class for service providers * tasked with registering classes that implement interfaces. * - * @since DOKAN_SINCE + * @since 3.13.0 */ abstract class BaseServiceProvider extends AbstractServiceProvider { protected $services = []; diff --git a/includes/DependencyManagement/BootableServiceProvider.php b/includes/DependencyManagement/BootableServiceProvider.php index ba73e8a12c..7216b5bead 100644 --- a/includes/DependencyManagement/BootableServiceProvider.php +++ b/includes/DependencyManagement/BootableServiceProvider.php @@ -15,7 +15,7 @@ * Note that `AbstractInterfaceServiceProvider` likely serves as a better base class for service providers * tasked with registering classes that implement interfaces. * - * @since DOKAN_SINCE + * @since 3.13.0 */ abstract class BootableServiceProvider extends BaseServiceProvider implements BootableServiceProviderInterface { } diff --git a/includes/DependencyManagement/Container.php b/includes/DependencyManagement/Container.php index 3e039a1f78..97fefb13f2 100644 --- a/includes/DependencyManagement/Container.php +++ b/includes/DependencyManagement/Container.php @@ -11,7 +11,7 @@ * This class extends the original League's Container object by adding some functionality * that we need for Dokan. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class Container extends BaseContainer { diff --git a/includes/DependencyManagement/ContainerException.php b/includes/DependencyManagement/ContainerException.php index 04b0f6e1b3..1c493187ed 100644 --- a/includes/DependencyManagement/ContainerException.php +++ b/includes/DependencyManagement/ContainerException.php @@ -9,7 +9,7 @@ * Class ContainerException. * Used to signal error conditions related to the dependency injection container. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class ContainerException extends \Exception { /** diff --git a/includes/DependencyManagement/Providers/ServiceProvider.php b/includes/DependencyManagement/Providers/ServiceProvider.php index b46c23dead..19c5e1696d 100644 --- a/includes/DependencyManagement/Providers/ServiceProvider.php +++ b/includes/DependencyManagement/Providers/ServiceProvider.php @@ -11,7 +11,7 @@ * This service provider handles the core services with the Dokan's * dependency injection container. * - * @since DOKAN_SINCE + * @since 3.13.0 */ class ServiceProvider extends BootableServiceProvider { /** diff --git a/languages/dokan-lite.pot b/languages/dokan-lite.pot index c24a72117b..74c9a11426 100644 --- a/languages/dokan-lite.pot +++ b/languages/dokan-lite.pot @@ -1,14 +1,14 @@ # Copyright (c) 2024 weDevs Pte. Ltd. All Rights Reserved. msgid "" msgstr "" -"Project-Id-Version: Dokan 3.12.6\n" +"Project-Id-Version: Dokan 3.13.0\n" "Report-Msgid-Bugs-To: https://dokan.co/contact/\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2024-10-24T07:08:11+00:00\n" +"POT-Creation-Date: 2024-10-28T14:59:30+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.11.0\n" "X-Domain: dokan-lite\n" @@ -42,31 +42,6 @@ msgstr "" msgid "https://dokan.co/" msgstr "" -#. translators: 1: Required PHP Version 2: Running php version -#: dokan.php:203 -msgid "The Minimum PHP Version Requirement for Dokan is %1$s. You are Running PHP %2$s" -msgstr "" - -#: dokan.php:512 -msgid "Get Pro" -msgstr "" - -#: dokan.php:515 -#: includes/Admin/AdminBar.php:81 -#: includes/Admin/Menu.php:68 -#: includes/Dashboard/Templates/Settings.php:60 -#: includes/Dashboard/Templates/Settings.php:67 -#: includes/functions-dashboard-navigation.php:58 -#: assets/js/vue-admin.js:2 -msgid "Settings" -msgstr "" - -#: dokan.php:516 -#: templates/admin-header.php:63 -#: assets/js/vue-admin.js:2 -msgid "Documentation" -msgstr "" - #. translators: 1) interval period #: includes/Abstracts/DokanBackgroundProcesses.php:87 msgid "Every %d Minutes" @@ -149,6 +124,15 @@ msgstr "" msgid "PRO Features" msgstr "" +#: includes/Admin/AdminBar.php:81 +#: includes/Admin/Menu.php:68 +#: includes/Dashboard/Templates/Settings.php:60 +#: includes/Dashboard/Templates/Settings.php:67 +#: includes/functions-dashboard-navigation.php:58 +#: assets/js/vue-admin.js:2 +msgid "Settings" +msgstr "" + #: includes/Admin/AdminBar.php:136 msgid "Visit Shop" msgstr "" @@ -1407,14 +1391,14 @@ msgstr "" #: includes/Admin/UserProfile.php:38 #: includes/Ajax.php:142 -#: includes/Assets.php:588 +#: includes/Assets.php:596 #: assets/js/vue-admin.js:2 #: assets/js/vue-bootstrap.js:2 msgid "Available" msgstr "" #: includes/Admin/UserProfile.php:39 -#: includes/Assets.php:589 +#: includes/Assets.php:597 #: assets/js/vue-admin.js:2 #: assets/js/vue-bootstrap.js:2 msgid "Not Available" @@ -1838,23 +1822,23 @@ msgstr "" msgid "id param is required" msgstr "" -#: includes/Assets.php:148 +#: includes/Assets.php:151 msgid "Could not find any vendor." msgstr "" -#: includes/Assets.php:149 +#: includes/Assets.php:152 msgid "Searching vendors" msgstr "" -#: includes/Assets.php:150 +#: includes/Assets.php:153 msgid "Search vendors" msgstr "" -#: includes/Assets.php:151 +#: includes/Assets.php:154 msgid "Are you sure ?" msgstr "" -#: includes/Assets.php:591 +#: includes/Assets.php:599 #: includes/Product/functions.php:504 #: templates/products/products-listing.php:109 #: assets/js/dokan-promo-notice.js:2 @@ -1863,76 +1847,76 @@ msgstr "" msgid "Are you sure?" msgstr "" -#: includes/Assets.php:592 +#: includes/Assets.php:600 msgid "Something went wrong. Please try again." msgstr "" -#: includes/Assets.php:606 +#: includes/Assets.php:614 msgid "Are you sure you want to revoke access to this download?" msgstr "" -#: includes/Assets.php:607 +#: includes/Assets.php:615 msgid "Could not grant access - the user may already have permission for this file or billing email is not set. Ensure the billing email is set, and the order has been saved." msgstr "" -#: includes/Assets.php:737 +#: includes/Assets.php:745 msgctxt "time constant" msgid "am" msgstr "" -#: includes/Assets.php:738 +#: includes/Assets.php:746 msgctxt "time constant" msgid "pm" msgstr "" -#: includes/Assets.php:739 +#: includes/Assets.php:747 msgctxt "time constant" msgid "AM" msgstr "" -#: includes/Assets.php:740 +#: includes/Assets.php:748 msgctxt "time constant" msgid "PM" msgstr "" -#: includes/Assets.php:741 +#: includes/Assets.php:749 msgctxt "time constant" msgid "hr" msgstr "" -#: includes/Assets.php:742 +#: includes/Assets.php:750 msgctxt "time constant" msgid "hrs" msgstr "" -#: includes/Assets.php:743 +#: includes/Assets.php:751 msgctxt "time constant" msgid "mins" msgstr "" -#: includes/Assets.php:746 +#: includes/Assets.php:754 #: templates/products/edit-product-single.php:306 #: templates/products/new-product.php:252 #: templates/products/tmpl-add-product-popup.php:88 msgid "To" msgstr "" -#: includes/Assets.php:748 +#: includes/Assets.php:756 #: templates/products/edit-product-single.php:299 #: templates/products/new-product.php:245 #: templates/products/tmpl-add-product-popup.php:81 msgid "From" msgstr "" -#: includes/Assets.php:749 +#: includes/Assets.php:757 msgid " - " msgstr "" -#: includes/Assets.php:750 +#: includes/Assets.php:758 msgid "W" msgstr "" -#: includes/Assets.php:751 +#: includes/Assets.php:759 #: templates/orders/listing.php:20 #: templates/products/products-listing.php:108 #: templates/store-lists-filter.php:84 @@ -1940,94 +1924,94 @@ msgstr "" msgid "Apply" msgstr "" -#: includes/Assets.php:752 +#: includes/Assets.php:760 #: assets/js/vue-admin.js:2 msgid "Clear" msgstr "" -#: includes/Assets.php:753 +#: includes/Assets.php:761 #: includes/Withdraw/Hooks.php:68 msgid "Custom" msgstr "" -#: includes/Assets.php:755 +#: includes/Assets.php:763 msgid "Su" msgstr "" -#: includes/Assets.php:756 +#: includes/Assets.php:764 msgid "Mo" msgstr "" -#: includes/Assets.php:757 +#: includes/Assets.php:765 msgid "Tu" msgstr "" -#: includes/Assets.php:758 +#: includes/Assets.php:766 msgid "We" msgstr "" -#: includes/Assets.php:759 +#: includes/Assets.php:767 msgid "Th" msgstr "" -#: includes/Assets.php:760 +#: includes/Assets.php:768 msgid "Fr" msgstr "" -#: includes/Assets.php:761 +#: includes/Assets.php:769 msgid "Sa" msgstr "" -#: includes/Assets.php:764 +#: includes/Assets.php:772 msgid "January" msgstr "" -#: includes/Assets.php:765 +#: includes/Assets.php:773 msgid "February" msgstr "" -#: includes/Assets.php:766 +#: includes/Assets.php:774 msgid "March" msgstr "" -#: includes/Assets.php:767 +#: includes/Assets.php:775 msgid "April" msgstr "" -#: includes/Assets.php:768 +#: includes/Assets.php:776 msgid "May" msgstr "" -#: includes/Assets.php:769 +#: includes/Assets.php:777 msgid "June" msgstr "" -#: includes/Assets.php:770 +#: includes/Assets.php:778 msgid "July" msgstr "" -#: includes/Assets.php:771 +#: includes/Assets.php:779 msgid "August" msgstr "" -#: includes/Assets.php:772 +#: includes/Assets.php:780 msgid "September" msgstr "" -#: includes/Assets.php:773 +#: includes/Assets.php:781 msgid "October" msgstr "" -#: includes/Assets.php:774 +#: includes/Assets.php:782 msgid "November" msgstr "" -#: includes/Assets.php:775 +#: includes/Assets.php:783 msgid "December" msgstr "" -#: includes/Assets.php:779 -#: includes/Assets.php:1051 +#: includes/Assets.php:787 +#: includes/Assets.php:1059 #: includes/Product/functions.php:310 #: templates/my-orders.php:95 #: templates/orders/details.php:196 @@ -2047,19 +2031,19 @@ msgstr "" msgid "Cancel" msgstr "" -#: includes/Assets.php:780 +#: includes/Assets.php:788 #: includes/Product/functions.php:321 #: templates/orders/details.php:347 #: templates/settings/store-form.php:38 msgid "Close" msgstr "" -#: includes/Assets.php:781 -#: includes/Assets.php:1050 +#: includes/Assets.php:789 +#: includes/Assets.php:1058 msgid "OK" msgstr "" -#: includes/Assets.php:782 +#: includes/Assets.php:790 #: includes/Vendor/SettingsApi/Settings/Pages/Store.php:283 #: includes/Vendor/SettingsApi/Settings/Pages/Store.php:430 #: includes/Vendor/SettingsApi/Settings/Pages/Store.php:446 @@ -2069,312 +2053,312 @@ msgstr "" msgid "No" msgstr "" -#: includes/Assets.php:783 +#: includes/Assets.php:791 msgid "Close this dialog" msgstr "" -#: includes/Assets.php:798 +#: includes/Assets.php:806 msgid "This field is required" msgstr "" -#: includes/Assets.php:799 +#: includes/Assets.php:807 msgid "Please fix this field." msgstr "" -#: includes/Assets.php:800 +#: includes/Assets.php:808 msgid "Please enter a valid email address." msgstr "" -#: includes/Assets.php:801 +#: includes/Assets.php:809 msgid "Please enter a valid URL." msgstr "" -#: includes/Assets.php:802 +#: includes/Assets.php:810 msgid "Please enter a valid date." msgstr "" -#: includes/Assets.php:803 +#: includes/Assets.php:811 msgid "Please enter a valid date (ISO)." msgstr "" -#: includes/Assets.php:804 +#: includes/Assets.php:812 msgid "Please enter a valid number." msgstr "" -#: includes/Assets.php:805 +#: includes/Assets.php:813 msgid "Please enter only digits." msgstr "" -#: includes/Assets.php:806 +#: includes/Assets.php:814 msgid "Please enter a valid credit card number." msgstr "" -#: includes/Assets.php:807 +#: includes/Assets.php:815 msgid "Please enter the same value again." msgstr "" -#: includes/Assets.php:808 +#: includes/Assets.php:816 msgid "Please enter no more than {0} characters." msgstr "" -#: includes/Assets.php:809 +#: includes/Assets.php:817 msgid "Please enter at least {0} characters." msgstr "" -#: includes/Assets.php:810 +#: includes/Assets.php:818 msgid "Please enter a value between {0} and {1} characters long." msgstr "" -#: includes/Assets.php:811 +#: includes/Assets.php:819 msgid "Please enter a value between {0} and {1}." msgstr "" -#: includes/Assets.php:812 +#: includes/Assets.php:820 msgid "Please enter a value less than or equal to {0}." msgstr "" -#: includes/Assets.php:813 +#: includes/Assets.php:821 msgid "Please enter a value greater than or equal to {0}." msgstr "" -#: includes/Assets.php:975 +#: includes/Assets.php:983 msgid "Upload featured image" msgstr "" -#: includes/Assets.php:976 +#: includes/Assets.php:984 msgid "Choose a file" msgstr "" -#: includes/Assets.php:977 +#: includes/Assets.php:985 msgid "Add Images to Product Gallery" msgstr "" -#: includes/Assets.php:978 +#: includes/Assets.php:986 msgid "Set featured image" msgstr "" -#: includes/Assets.php:979 +#: includes/Assets.php:987 #: includes/woo-views/html-product-download.php:8 msgid "Insert file URL" msgstr "" -#: includes/Assets.php:980 +#: includes/Assets.php:988 msgid "Add to gallery" msgstr "" -#: includes/Assets.php:981 +#: includes/Assets.php:989 msgid "Sorry, this attribute option already exists, Try a different one." msgstr "" -#: includes/Assets.php:982 +#: includes/Assets.php:990 msgid "Warning! This product will not have any variations if this option is not checked." msgstr "" -#: includes/Assets.php:983 +#: includes/Assets.php:991 msgid "Enter a name for the new attribute term:" msgstr "" -#: includes/Assets.php:984 +#: includes/Assets.php:992 msgid "Remove this attribute?" msgstr "" #. translators: %d: max linked variation. -#: includes/Assets.php:993 +#: includes/Assets.php:1001 msgid "Are you sure you want to link all variations? This will create a new variation for each and every possible combination of variation attributes (max %d per run)." msgstr "" -#: includes/Assets.php:994 +#: includes/Assets.php:1002 msgid "Enter a value" msgstr "" -#: includes/Assets.php:995 +#: includes/Assets.php:1003 msgid "Variation menu order (determines position in the list of variations)" msgstr "" -#: includes/Assets.php:996 +#: includes/Assets.php:1004 msgid "Enter a value (fixed or %)" msgstr "" -#: includes/Assets.php:997 +#: includes/Assets.php:1005 msgid "Are you sure you want to delete all variations? This cannot be undone." msgstr "" -#: includes/Assets.php:998 +#: includes/Assets.php:1006 msgid "Last warning, are you sure?" msgstr "" -#: includes/Assets.php:999 +#: includes/Assets.php:1007 msgid "Choose an image" msgstr "" -#: includes/Assets.php:1000 +#: includes/Assets.php:1008 msgid "Set variation image" msgstr "" -#: includes/Assets.php:1001 +#: includes/Assets.php:1009 msgid "variation added" msgstr "" -#: includes/Assets.php:1002 +#: includes/Assets.php:1010 msgid "variations added" msgstr "" -#: includes/Assets.php:1003 +#: includes/Assets.php:1011 msgid "No variations added" msgstr "" -#: includes/Assets.php:1004 +#: includes/Assets.php:1012 msgid "Are you sure you want to remove this variation?" msgstr "" -#: includes/Assets.php:1005 +#: includes/Assets.php:1013 msgid "Sale start date (YYYY-MM-DD format or leave blank)" msgstr "" -#: includes/Assets.php:1006 +#: includes/Assets.php:1014 msgid "Sale end date (YYYY-MM-DD format or leave blank)" msgstr "" -#: includes/Assets.php:1007 +#: includes/Assets.php:1015 msgid "Save changes before changing page?" msgstr "" -#: includes/Assets.php:1008 +#: includes/Assets.php:1016 msgid "%qty% variation" msgstr "" -#: includes/Assets.php:1009 +#: includes/Assets.php:1017 msgid "%qty% variations" msgstr "" -#: includes/Assets.php:1010 +#: includes/Assets.php:1018 msgid "No Result Found" msgstr "" -#: includes/Assets.php:1011 +#: includes/Assets.php:1019 msgid "Please insert value less than the regular price!" msgstr "" #. translators: %s: decimal -#: includes/Assets.php:1013 -#: includes/Assets.php:1193 +#: includes/Assets.php:1021 +#: includes/Assets.php:1201 msgid "Please enter with one decimal point (%s) without thousand separators." msgstr "" #. translators: %s: price decimal separator -#: includes/Assets.php:1015 -#: includes/Assets.php:1195 +#: includes/Assets.php:1023 +#: includes/Assets.php:1203 msgid "Please enter with one monetary decimal point (%s) without thousand separators and currency symbols." msgstr "" -#: includes/Assets.php:1016 -#: includes/Assets.php:1196 +#: includes/Assets.php:1024 +#: includes/Assets.php:1204 msgid "Please enter in country code with two capital letters." msgstr "" -#: includes/Assets.php:1017 -#: includes/Assets.php:1197 +#: includes/Assets.php:1025 +#: includes/Assets.php:1205 msgid "Please enter in a value less than the regular price." msgstr "" -#: includes/Assets.php:1018 -#: includes/Assets.php:1198 +#: includes/Assets.php:1026 +#: includes/Assets.php:1206 msgid "This product has produced sales and may be linked to existing orders. Are you sure you want to delete it?" msgstr "" -#: includes/Assets.php:1019 -#: includes/Assets.php:1199 +#: includes/Assets.php:1027 +#: includes/Assets.php:1207 msgid "This action cannot be reversed. Are you sure you wish to erase personal data from the selected orders?" msgstr "" -#: includes/Assets.php:1029 +#: includes/Assets.php:1037 msgid "Select and Crop" msgstr "" -#: includes/Assets.php:1030 +#: includes/Assets.php:1038 msgid "Choose Image" msgstr "" -#: includes/Assets.php:1031 +#: includes/Assets.php:1039 msgid "Product title is required" msgstr "" -#: includes/Assets.php:1032 +#: includes/Assets.php:1040 msgid "Product category is required" msgstr "" -#: includes/Assets.php:1033 +#: includes/Assets.php:1041 msgid "Product created successfully" msgstr "" -#: includes/Assets.php:1037 +#: includes/Assets.php:1045 msgid "One result is available, press enter to select it." msgstr "" -#: includes/Assets.php:1038 +#: includes/Assets.php:1046 msgid "%qty% results are available, use up and down arrow keys to navigate." msgstr "" -#: includes/Assets.php:1039 +#: includes/Assets.php:1047 msgid "No matches found" msgstr "" -#: includes/Assets.php:1040 +#: includes/Assets.php:1048 msgid "Loading failed" msgstr "" -#: includes/Assets.php:1041 +#: includes/Assets.php:1049 msgid "Please enter 1 or more characters" msgstr "" -#: includes/Assets.php:1042 +#: includes/Assets.php:1050 msgid "Please enter %qty% or more characters" msgstr "" -#: includes/Assets.php:1043 +#: includes/Assets.php:1051 msgid "Please delete 1 character" msgstr "" -#: includes/Assets.php:1044 +#: includes/Assets.php:1052 msgid "Please delete %qty% characters" msgstr "" -#: includes/Assets.php:1045 +#: includes/Assets.php:1053 msgid "You can only select 1 item" msgstr "" -#: includes/Assets.php:1046 +#: includes/Assets.php:1054 msgid "You can only select %qty% items" msgstr "" -#: includes/Assets.php:1047 +#: includes/Assets.php:1055 msgid "Loading more results…" msgstr "" -#: includes/Assets.php:1048 +#: includes/Assets.php:1056 msgid "Searching…" msgstr "" -#: includes/Assets.php:1049 +#: includes/Assets.php:1057 msgid "Calculating" msgstr "" -#: includes/Assets.php:1052 +#: includes/Assets.php:1060 msgid "Attribute Name" msgstr "" -#: includes/Assets.php:1054 +#: includes/Assets.php:1062 msgid "Are you sure? You have uploaded banner but didn't click the Update Settings button!" msgstr "" -#: includes/Assets.php:1055 +#: includes/Assets.php:1063 #: templates/settings/header.php:20 #: templates/settings/payment-manage.php:48 #: templates/settings/store-form.php:270 msgid "Update Settings" msgstr "" -#: includes/Assets.php:1057 +#: includes/Assets.php:1065 msgid "Please enter 3 or more characters" msgstr "" @@ -7765,6 +7749,11 @@ msgstr "" msgid "Community" msgstr "" +#: templates/admin-header.php:63 +#: assets/js/vue-admin.js:2 +msgid "Documentation" +msgstr "" + #: templates/admin-header.php:72 msgid "FAQ" msgstr "" diff --git a/package-lock.json b/package-lock.json index 8fb169b256..79eec343fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dokan", - "version": "3.12.6", + "version": "3.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dokan", - "version": "3.12.6", + "version": "3.13.0", "license": "GPL", "dependencies": { "@wordpress/i18n": "^5.8.0" diff --git a/package.json b/package.json index 64d4f9b513..e5fc546734 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dokan", - "version": "3.12.6", + "version": "3.13.0", "description": "A WordPress marketplace plugin", "author": "weDevs", "license": "GPL", diff --git a/readme.txt b/readme.txt index 8fa46277be..d3fd9f452f 100644 --- a/readme.txt +++ b/readme.txt @@ -7,7 +7,7 @@ Tested up to: 6.6.2 WC requires at least: 8.0.0 WC tested up to: 9.3.3 Requires PHP: 7.4 -Stable tag: 3.12.6 +Stable tag: 3.13.0 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -347,6 +347,11 @@ A. Just install and activate the PRO version without deleting the free plugin. A == Changelog == += v3.13.0 ( Oct 28, 2024 ) = + +- **feat:** Replaced the Dokan array container with the League Container, ensuring backward compatibility for seamless performance and enhanced flexibility. +- **feat:** Updated Dokan to be fully compatible with WooCommerce Analytics Reports + = v3.12.6 ( Oct 24, 2024 ) = - **fix:** Fixed js error on frontend pages. diff --git a/templates/whats-new.php b/templates/whats-new.php index 6377769361..03ff8ac398 100644 --- a/templates/whats-new.php +++ b/templates/whats-new.php @@ -3,6 +3,22 @@ * When you are adding new version please follow this sequence for changes: New Feature, New, Improvement, Fix... */ $changelog = [ + [ + 'version' => 'Version 3.13.0', + 'released' => '2024-10-28', + 'changes' => [ + 'New' => [ + [ + 'title' => 'Replaced the Dokan array container with the League Container, ensuring backward compatibility for seamless performance and enhanced flexibility.', + 'description' => '', + ], + [ + 'title' => 'Updated Dokan to be fully compatible with WooCommerce Analytics Reports', + 'description' => '', + ], + ], + ], + ], [ 'version' => 'Version 3.12.6', 'released' => '2024-10-24', diff --git a/tests/php/src/Analytics/Reports/StockStatsQueryFilterTest.php b/tests/php/src/Analytics/Reports/StockStatsQueryFilterTest.php index 29e34b7093..7c1db3ae04 100644 --- a/tests/php/src/Analytics/Reports/StockStatsQueryFilterTest.php +++ b/tests/php/src/Analytics/Reports/StockStatsQueryFilterTest.php @@ -1,6 +1,6 @@ Date: Wed, 30 Oct 2024 17:03:09 +0600 Subject: [PATCH 12/12] Fix/revenue reports (#2423) * Update order stats for fixing the coupon issue * revert: remove analytics coupon hide scripts and make coupons grid visible for analytics revenue page. * Consider refund type for net sales * Fix order count * Fix avg amount --------- Co-authored-by: MdAsifHossainNadim --- assets/src/js/dokan-admin-analytics.js | 6 -- .../Reports/Orders/Stats/QueryFilter.php | 77 +++++++++++++++---- includes/Assets.php | 8 -- webpack.config.js | 1 - 4 files changed, 64 insertions(+), 28 deletions(-) delete mode 100644 assets/src/js/dokan-admin-analytics.js diff --git a/assets/src/js/dokan-admin-analytics.js b/assets/src/js/dokan-admin-analytics.js deleted file mode 100644 index b3ae8c54e9..0000000000 --- a/assets/src/js/dokan-admin-analytics.js +++ /dev/null @@ -1,6 +0,0 @@ -// TODO: Remove this file after introducing Dokan coupon distribution. -wp.hooks.addFilter( - 'woocommerce_admin_revenue_report_charts', - 'dokan/remove-woocommerce-revenue-coupon-data-from/callback', - ( data ) => data.filter( item => item.key !== 'coupons' ) -); diff --git a/includes/Analytics/Reports/Orders/Stats/QueryFilter.php b/includes/Analytics/Reports/Orders/Stats/QueryFilter.php index 3a242d4b79..426bc3c7ff 100644 --- a/includes/Analytics/Reports/Orders/Stats/QueryFilter.php +++ b/includes/Analytics/Reports/Orders/Stats/QueryFilter.php @@ -3,6 +3,7 @@ namespace WeDevs\Dokan\Analytics\Reports\Orders\Stats; use WeDevs\Dokan\Analytics\Reports\Orders\QueryFilter as OrdersQueryFilter; +use WeDevs\Dokan\Analytics\Reports\OrderType; /** * Class QueryFilter @@ -29,8 +30,11 @@ public function register_hooks(): void { add_filter( 'woocommerce_analytics_clauses_join_orders_stats_total', [ $this, 'add_join_subquery' ] ); add_filter( 'woocommerce_analytics_clauses_join_orders_stats_interval', [ $this, 'add_join_subquery' ] ); - add_filter( 'woocommerce_analytics_clauses_where_orders_stats_total', [ $this, 'add_where_subquery' ], 30 ); - add_filter( 'woocommerce_analytics_clauses_where_orders_stats_interval', [ $this, 'add_where_subquery' ], 30 ); + // add_filter( 'woocommerce_analytics_clauses_where_orders_stats_total', [ $this, 'add_where_subquery' ], 30 ); + // add_filter( 'woocommerce_analytics_clauses_where_orders_stats_interval', [ $this, 'add_where_subquery' ], 30 ); + + add_filter( 'woocommerce_analytics_clauses_where_orders_stats_total', [ $this, 'add_where_subquery_for_vendor_filter' ], 30 ); + add_filter( 'woocommerce_analytics_clauses_where_orders_stats_interval', [ $this, 'add_where_subquery_for_vendor_filter' ], 30 ); add_filter( 'woocommerce_analytics_clauses_select_orders_stats_total', [ $this, 'add_select_subquery_for_total' ] ); add_filter( 'woocommerce_analytics_clauses_select_orders_stats_interval', [ $this, 'add_select_subquery_for_total' ] ); @@ -51,21 +55,61 @@ public function register_hooks(): void { * * @return array Modified report columns. */ - public function modify_admin_report_columns( array $column, string $context, string $wc_table_name ): array { + public function modify_admin_report_columns( array $column, string $context, string $table_name ): array { if ( $context !== $this->context ) { return $column; } - $table_name = $this->get_dokan_table(); - $types = $this->get_order_types_for_sql_excluding_refunds(); - - $order_count = "SUM( CASE WHEN {$table_name}.order_type IN($types) THEN 1 ELSE 0 END )"; - - $column['orders_count'] = "{$order_count} as orders_count"; - $column['avg_items_per_order'] = "SUM( {$wc_table_name}.num_items_sold ) / {$order_count} AS avg_items_per_order"; - $column['avg_order_value'] = "SUM( {$wc_table_name}.net_total ) / {$order_count} AS avg_order_value"; - $column['avg_admin_commission'] = "SUM( {$table_name}.admin_commission ) / {$order_count} AS avg_admin_commission"; - $column['avg_vendor_earning'] = "SUM( {$table_name}.vendor_earning ) / {$order_count} AS avg_vendor_earning"; + $dokan_table_name = $this->get_dokan_table(); + $order_types = $this->get_order_types_for_sql_excluding_refunds(); + $types = implode( ',', ( new OrderType() )->get_vendor_order_types() ); + // $types = $this->get_order_types_for_sql_excluding_refunds(); + + $parent_order_types_str = implode( ',', ( new OrderType() )->get_admin_order_types_excluding_refunds() ); + $refund_order_types_str = implode( ',', ( new OrderType() )->get_vendor_refund_types() ); + + $order_count = "SUM( CASE WHEN {$dokan_table_name}.order_type IN($order_types) THEN 1 ELSE 0 END )"; + + /** + * Override WC column. + * + * We can apply the common where clause after Dokan Coupon Distribution. + * File to restore: @see https://github.com/getdokan/dokan/blob/2cffa360a94b32033e7591fece5950068ab758f5/includes/Analytics/Reports/Orders/Stats/QueryFilter.php#L4 + */ + $coupon = "SUM(CASE WHEN {$dokan_table_name}.order_type IN($parent_order_types_str) THEN discount_amount END)"; + + $net_total = "SUM(CASE WHEN {$dokan_table_name}.order_type IN($types) THEN {$table_name}.net_total END)"; + + $item_sold = "SUM( CASE WHEN {$dokan_table_name}.order_type IN($types) THEN {$table_name}.num_items_sold END)"; + + $refunds = "ABS( SUM( CASE WHEN {$table_name}.net_total < 0 AND {$dokan_table_name}.order_type IN($refund_order_types_str) THEN {$table_name}.net_total ELSE 0 END ) )"; + + $gross_sales = + "( SUM( CASE WHEN {$dokan_table_name}.order_type IN($types) THEN {$table_name}.total_sales END )" . + " + COALESCE( $coupon, 0 )" . // SUM() all nulls gives null. + " - SUM(CASE WHEN {$dokan_table_name}.order_type IN($types) THEN {$table_name}.tax_total END)" . + " - SUM(CASE WHEN {$dokan_table_name}.order_type IN($types) THEN {$table_name}.shipping_total END)" . + " + {$refunds}" . + ' ) as gross_sales'; + + $column['num_items_sold'] = "$item_sold as num_items_sold"; + $column['gross_sales'] = $gross_sales; + $column['total_sales'] = "SUM( CASE WHEN {$dokan_table_name}.order_type IN($types) THEN {$table_name}.total_sales END) AS total_sales"; + $column['coupons'] = "COALESCE( $coupon, 0 ) AS coupons"; // SUM() all nulls gives null; + $column['coupons_count'] = 'COALESCE( coupons_count, 0 ) as coupons_count'; + $column['refunds'] = "{$refunds} AS refunds"; + $column['taxes'] = "SUM(CASE WHEN {$dokan_table_name}.order_type IN($types) THEN {$table_name}.tax_total END) AS taxes"; + $column['shipping'] = "SUM(CASE WHEN {$dokan_table_name}.order_type IN($types) THEN {$table_name}.shipping_total END) AS shipping"; + $column['net_revenue'] = " $net_total AS net_revenue"; + $column['total_customers'] = "COUNT( DISTINCT( {$table_name}.customer_id ) ) as total_customers"; + // End of override + + $column['orders_count'] = "{$order_count} as orders_count"; + + $column['avg_items_per_order'] = "{$item_sold} / {$order_count} AS avg_items_per_order"; + $column['avg_order_value'] = "{$net_total} / {$order_count} AS avg_order_value"; + $column['avg_admin_commission'] = "SUM( CASE WHEN {$dokan_table_name}.order_type IN($types) THEN {$dokan_table_name}.admin_commission END) / {$order_count} AS avg_admin_commission"; + $column['avg_vendor_earning'] = "SUM( CASE WHEN {$dokan_table_name}.order_type IN($types) THEN {$dokan_table_name}.vendor_earning END) / {$order_count} AS avg_vendor_earning"; return $column; } @@ -87,4 +131,11 @@ public function add_select_subquery_for_total( $clauses ) { return $clauses; } + + /** + * @inheritDoc + */ + public function add_where_subquery_for_vendor_filter( array $clauses ): array { + return parent::add_where_subquery_for_vendor_filter( $clauses ); + } } diff --git a/includes/Assets.php b/includes/Assets.php index e39d4d8a1f..596f0a429a 100644 --- a/includes/Assets.php +++ b/includes/Assets.php @@ -129,9 +129,6 @@ public function enqueue_admin_scripts( $hook ) { wp_localize_script( 'dokan-admin-product', 'dokan_admin_product', $this->admin_product_localize_scripts() ); } - // Load admin scripts for analytics. - wp_enqueue_script( 'dokan-admin-analytics' ); - do_action( 'dokan_enqueue_admin_scripts', $hook ); } @@ -557,11 +554,6 @@ public function get_scripts() { 'deps' => [ 'jquery' ], 'version' => filemtime( $asset_path . 'js/dokan-frontend.js' ), ], - 'dokan-admin-analytics' => [ - 'src' => $asset_url . '/js/dokan-admin-analytics.js', - 'deps' => [ 'wc-admin-app', 'wp-hooks' ], - 'version' => filemtime( $asset_path . 'js/dokan-admin-analytics.js' ), - ], ]; return $scripts; diff --git a/webpack.config.js b/webpack.config.js index 3517e4d421..d0c8433caa 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -40,7 +40,6 @@ const entryPoint = { ], 'helper': './assets/src/js/helper.js', 'dokan-frontend': './assets/src/js/dokan-frontend.js', - 'dokan-admin-analytics': './assets/src/js/dokan-admin-analytics.js', 'style': '/assets/src/less/style.less', 'rtl': '/assets/src/less/rtl.less',