diff --git a/.env.example b/.env.example index d8dfa30e..fdbfe9b0 100644 --- a/.env.example +++ b/.env.example @@ -12,3 +12,4 @@ VUE_APP_JOB_TITLES={"JOB_IMP_PROD_NEW":"Import products","JOB_IMP_PROD_UPD":"Syn VUE_APP_INITIAL_JOB_TYPES={"JOB_IMP_PROD_NEW_BLK":"products","JOB_IMP_ORD_BLK":"orders"} VUE_APP_BASE_URL= VUE_APP_BATCH_JOB_ENUMS={"JOB_BKR_ORD_UNF":{"id":"JOB_BKR_ORD_UNF","facilityId":"_NA_","unfillable": true},"JOB_BKR_ORD":{"id": "JOB_BKR_ORD","facilityId":"_NA_","unfillable": false},"JOB_BKR_PREORD_UNF":{"id":"JOB_BKR_PREORD_UNF","facilityId":"PRE_ORDER_PARKING","unfillable":true},"JOB_BKR_PREORD":{"id":"JOB_BKR_PREORD","facilityId":"PRE_ORDER_PARKING","unfillable":false},"JOB_BKR_BACKORD_UNF":{"id":"JOB_BKR_BACKORD_UNF","facilityId":"BACKORDER_PARKING","unfillable":true},"JOB_BKR_BACKORD":{"id":"JOB_BKR_BACKORD","facilityId":"BACKORDER_PARKING","unfillable":false}} +VUE_APP_WEBHOOK_ENUMS={"NEW_PRODUCTS":"products/create","DELETE_PRODUCTS":"products/update","NEW_ORDERS":"orders/create","CANCELLED_ORDERS":"orders/cancelled","PAYMENT_STATUS":"orders/paid","RETURNS":"","BULK_OPERATIONS_FINISH":"bulk_operations/finish"} diff --git a/src/locales/en.json b/src/locales/en.json index ae3f8ed1..c72b51ea 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -42,6 +42,7 @@ "Daily": "Daily", "Dashoard": "Dashoard", "Days": "Days", + "Delete products": "Delete products", "Disable": "Disable", "Dismiss": "Dismiss", "Disabled": "Disabled", @@ -53,6 +54,7 @@ "Every 5 minutes": "Every 5 minutes", "Every 15 minutes": "Every 15 minutes", "Every 30 minutes": "Every 30 minutes", + "File upload status": "File upload status", "Fulfilled": "Fulfilled", "Fulfillment status": "Fulfillment status", "Hard sync": "Hard sync", @@ -78,6 +80,7 @@ "More options": "More options", "New broker run": "New broker run", "New orders": "New orders", + "New products": "New products", "No jobs have run yet": "No jobs have run yet", "No previous occurrence": "No previous occurrence", "Notes": "Notes", @@ -173,10 +176,13 @@ "Update time zone": "Update time zone", "Use POs to manage catalog": "Use POs to manage catalog", "Username": "Username", + "Webhook subscribed successfully": "Webhook subscribed successfully", + "Webhook unsubscribed successfully": "Webhook unsubscribed successfully", "Would you like to update your time zone to . Your profile is currently set to . This setting can always be changed from the settings menu.": "Would you like to update your time zone to {localTimeZone}. Your profile is currently set to {profileTimeZone}. This setting can always be changed from the settings menu.", "Unfillable": "Unfillable", "Unfillable orders": "Unfillable orders", "Unfulfilled orders that pass their auto cancelation date will be canceled automatically in HotWax Commerce. They will also be canceled in Shopify if upload for canceled orders is enabled.": "Unfulfilled orders that pass their auto cancelation date will be canceled automatically in HotWax Commerce. They will also be canceled in Shopify if upload for canceled orders is enabled.", + "Webhooks": "Webhooks", "Upload Pending Process": "Upload Pending Process", "Update shipping dates in Shopify": "Update shipping dates in Shopify", "When importing historical completed orders, this should be turned off.": "When importing historical completed orders, this should be turned off.", diff --git a/src/services/WebhookService.ts b/src/services/WebhookService.ts new file mode 100644 index 00000000..d50b1c8c --- /dev/null +++ b/src/services/WebhookService.ts @@ -0,0 +1,90 @@ +import api from '@/api' + +const fetchShopifyWebhooks = async (payload?: any): Promise => { + return api({ + url: "/service/getShopifyWebhooks", + method: "post", + data: payload + }); +} + +// TODO: add the service endpoint for the new order webhook +const subscribeNewOrderWebhook = async (payload?: any): Promise => { + return api ({ + url: '', + method: 'post', + data: payload + }) +} + +// TODO: add the service endpoint for the cancelled order webhook +const subscribeCancelledOrderWebhook = async (payload?: any): Promise => { + return api ({ + url: '', + method: 'post', + data: payload + }) +} + +// TODO: add the service endpoint for the payment status webhook +const subscribePaymentStatusWebhook = async (payload?: any): Promise => { + return api ({ + url: '', + method: 'post', + data: payload + }) +} + +// TODO: add the service endpoint for the order return webhook +const subscribeReturnWebhook = async (payload?: any): Promise => { + return api ({ + url: '', + method: 'post', + data: payload + }) +} + +// TODO: add the service endpoint for the new product webhook +const subscribeNewProductsWebhook = async (payload?: any): Promise => { + return api ({ + url: '', + method: 'post', + data: payload + }) +} + +const subscribeDeleteProductsWebhook = async (payload?: any): Promise => { + return api ({ + url: 'service/subscribeProductDeleteWebhook', + method: 'post', + data: payload + }) +} + +const subscribeFileStatusUpdateWebhook = async (payload?: any): Promise => { + return api ({ + url: 'service/subscribeFileStatusUpdateWebhook', + method: 'post', + data: payload + }) +} + +const unsubscribeWebhook = async (payload?: any): Promise => { + return api ({ + url: 'service/removeShopifyWebhook', + method: 'post', + data: payload + }) +} + +export const WebhookService = { + fetchShopifyWebhooks, + subscribeNewOrderWebhook, + subscribeCancelledOrderWebhook, + subscribeFileStatusUpdateWebhook, + subscribePaymentStatusWebhook, + subscribeReturnWebhook, + subscribeNewProductsWebhook, + subscribeDeleteProductsWebhook, + unsubscribeWebhook +} \ No newline at end of file diff --git a/src/store/index.ts b/src/store/index.ts index 3fb81574..f100639e 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -8,6 +8,7 @@ import userModule from './modules/user'; import productModule from "./modules/product" import jobModule from "./modules/job" import utilModule from "./modules/util" +import webhookModule from "./modules/webhook" // TODO check how to register it from the components only @@ -35,7 +36,8 @@ const store = createStore({ 'user': userModule, 'product': productModule, 'job': jobModule, - 'util': utilModule + 'util': utilModule, + 'webhook': webhookModule }, }) diff --git a/src/store/modules/webhook/WebhookState.ts b/src/store/modules/webhook/WebhookState.ts new file mode 100644 index 00000000..dc12e384 --- /dev/null +++ b/src/store/modules/webhook/WebhookState.ts @@ -0,0 +1,3 @@ +export default interface WebhookState { + cached: any +} \ No newline at end of file diff --git a/src/store/modules/webhook/actions.ts b/src/store/modules/webhook/actions.ts new file mode 100644 index 00000000..656789c7 --- /dev/null +++ b/src/store/modules/webhook/actions.ts @@ -0,0 +1,78 @@ +import { ActionTree } from "vuex"; +import RootState from "@/store/RootState"; +import WebhookState from "./WebhookState"; +import { WebhookService } from "@/services/WebhookService"; +import { hasError, showToast } from "@/utils"; +import * as types from './mutations-types' +import { translate } from '@/i18n' + +const actions: ActionTree = { + async fetchWebhooks({ commit }) { + await WebhookService.fetchShopifyWebhooks({ shopifyConfigId: this.state.user.shopifyConfig }).then(resp => { + if (resp.status == 200 && resp.data.webhooks?.length > 0 && !hasError(resp)) { + const webhooks = resp.data.webhooks; + const topics: any = {} + webhooks.map((webhook: any) => { + topics[webhook.topic] = webhook + }) + commit(types.WEBHOOK_UPDATED, topics) + } + }).catch(err => console.error(err)) + }, + async unsubscribeWebhook({ dispatch }, payload: any) { + + let resp; + + try { + resp = await WebhookService.unsubscribeWebhook(payload) + if (resp.status === 200 && !hasError(resp)) { + showToast(translate("Webhook unsubscribed successfully")); + } + } catch(err) { + console.log(err) + showToast(translate("Something went wrong")); + } finally { + dispatch('fetchWebhooks') + } + }, + async subscribeWebhook({ dispatch }, id: string) { + + // stores the webhook service that needs to be called on the basis of current webhook selected, doing + // so as we have defined separate service for different webhook subscription + const webhookMethods = { + 'NEW_ORDERS': WebhookService.subscribeNewOrderWebhook, + 'CANCELLED_ORDERS': WebhookService.subscribeCancelledOrderWebhook, + 'PAYMENT_STATUS':WebhookService.subscribePaymentStatusWebhook, + 'RETURNS': WebhookService.subscribeReturnWebhook, + 'NEW_PRODUCTS': WebhookService.subscribeNewProductsWebhook, + 'DELETE_PRODUCTS': WebhookService.subscribeDeleteProductsWebhook, + 'BULK_OPERATIONS_FINISH': WebhookService.subscribeFileStatusUpdateWebhook + } as any + const webhookMethod: any = webhookMethods[id]; + + if (!webhookMethod) { + showToast(translate("Configuration missing")); + return; + } + + let resp; + + try { + resp = await webhookMethod({ shopifyConfigId: this.state.user.shopifyConfig }) + + if (resp.status == 200 && !hasError(resp)) { + showToast(translate('Webhook subscribed successfully')) + } else { + showToast(translate('Something went wrong')) + console.error(resp) + } + } catch (err) { + showToast(translate('Something went wrong')) + console.error(err); + } finally { + await dispatch('fetchWebhooks') + } + } +} + +export default actions \ No newline at end of file diff --git a/src/store/modules/webhook/getters.ts b/src/store/modules/webhook/getters.ts new file mode 100644 index 00000000..c259e03a --- /dev/null +++ b/src/store/modules/webhook/getters.ts @@ -0,0 +1,9 @@ +import { GetterTree } from "vuex"; +import RootState from "@/store/RootState"; +import WebhookState from "./WebhookState"; + +const getters: GetterTree = { + getCachedWebhook: (state) => state.cached +} + +export default getters \ No newline at end of file diff --git a/src/store/modules/webhook/index.ts b/src/store/modules/webhook/index.ts new file mode 100644 index 00000000..2b93db9c --- /dev/null +++ b/src/store/modules/webhook/index.ts @@ -0,0 +1,18 @@ +import { Module } from 'vuex' +import WebhookState from './WebhookState' +import RootState from '@/store/RootState' +import getters from './getters' +import mutations from './mutations' +import actions from './actions' + +const webhookModule: Module = { + namespaced: true, + state: { + cached: {} + }, + getters, + actions, + mutations, +} + +export default webhookModule \ No newline at end of file diff --git a/src/store/modules/webhook/mutations-types.ts b/src/store/modules/webhook/mutations-types.ts new file mode 100644 index 00000000..7acbf857 --- /dev/null +++ b/src/store/modules/webhook/mutations-types.ts @@ -0,0 +1,2 @@ +export const SN_WEBHOOK = 'webhook' +export const WEBHOOK_UPDATED = SN_WEBHOOK + '/WEBHOOK_UPDATED' \ No newline at end of file diff --git a/src/store/modules/webhook/mutations.ts b/src/store/modules/webhook/mutations.ts new file mode 100644 index 00000000..e8dca8f1 --- /dev/null +++ b/src/store/modules/webhook/mutations.ts @@ -0,0 +1,11 @@ +import { MutationTree } from "vuex"; +import WebhookState from "./WebhookState"; +import * as types from './mutations-types' + +const mutations: MutationTree = { + [types.WEBHOOK_UPDATED] (state, payload: any) { + state.cached = payload + } +} + +export default mutations \ No newline at end of file diff --git a/src/views/InitialLoad.vue b/src/views/InitialLoad.vue index 47aacc3a..8a0ef95c 100644 --- a/src/views/InitialLoad.vue +++ b/src/views/InitialLoad.vue @@ -40,6 +40,10 @@ {{ $t("Process Uploads") }} + + {{ $t("File upload status") }} + + {{ $t("Upload Pending Process") }} @@ -69,6 +73,7 @@ import { IonMenuButton, IonPage, IonTitle, + IonToggle, IonToolbar, isPlatform } from '@ionic/vue'; @@ -96,11 +101,13 @@ export default defineComponent({ IonMenuButton, IonPage, IonTitle, + IonToggle, IonToolbar }, data() { return { jobEnums: JSON.parse(process.env?.VUE_APP_INITIAL_JOB_ENUMS as string) as any, + webhookEnums: JSON.parse(process.env?.VUE_APP_WEBHOOK_ENUMS as string) as any, currentSelectedJobModal: '', job: {} as any, lastShopifyOrderId: '', @@ -115,35 +122,41 @@ export default defineComponent({ "systemJobEnumId_op": "in" } }) + this.store.dispatch('webhook/fetchWebhooks') }, computed: { ...mapGetters({ getJobStatus: 'job/getJobStatus', getJob: 'job/getJob', shopifyConfigId: 'user/getShopifyConfigId', - currentEComStore: 'user/getCurrentEComStore' + currentEComStore: 'user/getCurrentEComStore', + getCachedWebhook: 'webhook/getCachedWebhook' }), + fileStatusUpdateWebhook(): boolean { + const webhookTopic = this.webhookEnums['BULK_OPERATIONS_FINISH'] + return this.getCachedWebhook[webhookTopic] + }, processPendingUploadsOnShopify(): boolean { const status = this.getJobStatus(this.jobEnums["UL_PRCS"]); return status && status !== "SERVICE_DRAFT"; - }, + } }, methods: { async updateJob(checked: boolean, id: string, status = 'EVERY_15_MIN') { const job = this.getJob(id); - // added check that if the job is not present, then display a toast and then return - if (!job) { - showToast(translate('Configuration missing')) - return; - } - // TODO: added this condition to not call the api when the value of the select automatically changes // need to handle this properly if ((checked && job?.status === 'SERVICE_PENDING') || (!checked && job?.status === 'SERVICE_DRAFT')) { return; } + // added check that if the job is not present, then display a toast and then return + if (!job) { + showToast(translate('Configuration missing')) + return; + } + job['jobStatus'] = status // if job runTime is not a valid date then making runTime as empty @@ -181,6 +194,21 @@ export default defineComponent({ emitter.emit('playAnimation'); this.isJobDetailAnimationCompleted = true; } + }, + async updateWebhook(checked: boolean, enumId: string) { + const webhook = this.getCachedWebhook[this.webhookEnums[enumId]] + + // TODO: added this condition to not call the api when the value of the select automatically changes + // need to handle this properly + if ((checked && webhook) || (!checked && !webhook)) { + return; + } + + if (checked) { + await this.store.dispatch('webhook/subscribeWebhook', enumId) + } else { + await this.store.dispatch('webhook/unsubscribeWebhook', { webhookId: webhook?.id, shopifyConfigId: this.shopifyConfigId }) + } } }, setup() { diff --git a/src/views/Orders.vue b/src/views/Orders.vue index 95975eb8..ff9ea06a 100644 --- a/src/views/Orders.vue +++ b/src/views/Orders.vue @@ -36,6 +36,28 @@ + + + {{ $t("Webhooks") }} + + + {{ $t("New orders") }} + + + + {{ $t("Cancelled orders") }} + + + + {{ $t("Payment status") }} + + + + {{ $t("Returns") }} + + + + {{ $t("Upload") }} @@ -192,6 +214,7 @@ export default defineComponent({ jobEnums: JSON.parse(process.env?.VUE_APP_ODR_JOB_ENUMS as string) as any, batchJobEnums: JSON.parse(process.env?.VUE_APP_BATCH_JOB_ENUMS as string) as any, jobFrequencyType: JSON.parse(process.env?.VUE_APP_JOB_FREQUENCY_TYPE as string) as any, + webhookEnums: JSON.parse(process.env?.VUE_APP_WEBHOOK_ENUMS as string) as any, currentJob: '' as any, title: 'New orders', currentJobStatus: '', @@ -207,7 +230,8 @@ export default defineComponent({ orderBatchJobs: "job/getOrderBatchJobs", shopifyConfigId: 'user/getShopifyConfigId', currentEComStore: 'user/getCurrentEComStore', - getTemporalExpr: 'job/getTemporalExpr' + getTemporalExpr: 'job/getTemporalExpr', + getCachedWebhook: 'webhook/getCachedWebhook' }), promiseDateChanges(): boolean { const status = this.getJobStatus(this.jobEnums['NTS_PRMS_DT_CHNG']); @@ -217,8 +241,39 @@ export default defineComponent({ const status = this.getJobStatus(this.jobEnums["AUTO_CNCL_DAL"]); return status && status !== "SERVICE_DRAFT"; }, + isNewOrders(): boolean { + const webhookTopic = this.webhookEnums['NEW_ORDERS'] + return this.getCachedWebhook[webhookTopic] + }, + isCancelledOrders(): boolean { + const webhookTopic = this.webhookEnums['CANCELLED_ORDERS'] + return this.getCachedWebhook[webhookTopic] + }, + isPaymentStatus(): boolean { + const webhookTopic = this.webhookEnums['PAYMENT_STATUS'] + return this.getCachedWebhook[webhookTopic] + }, + isReturns(): boolean { + const webhookTopic = this.webhookEnums['RETURNS'] + return this.getCachedWebhook[webhookTopic] + }, }, - methods: { + methods: { + async updateWebhook(checked: boolean, enumId: string) { + const webhook = this.getCachedWebhook[this.webhookEnums[enumId]] + + // TODO: added this condition to not call the api when the value of the select automatically changes + // need to handle this properly + if ((checked && webhook) || (!checked && !webhook)) { + return; + } + + if (checked) { + await this.store.dispatch('webhook/subscribeWebhook', enumId) + } else { + await this.store.dispatch('webhook/unsubscribeWebhook', { webhookId: webhook?.id, shopifyConfigId: this.shopifyConfigId }) + } + }, async addBatch() { const batchmodal = await modalController.create({ component: BatchModal @@ -368,6 +423,7 @@ export default defineComponent({ "systemJobEnumId_op": "in" } }); + this.store.dispatch('webhook/fetchWebhooks') }, setup() { const store = useStore(); diff --git a/src/views/Product.vue b/src/views/Product.vue index d180c554..01c9b781 100644 --- a/src/views/Product.vue +++ b/src/views/Product.vue @@ -26,6 +26,20 @@

{{ $t("Sync products and category structures from Shopify into HotWax Commerce and keep them up to date.") }}

+ + + + {{ $t("Webhooks") }} + + + {{ $t("New products") }} + + + + {{ $t("Delete products") }} + + +