diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 3afd4df24..26bcc25e1 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -123,6 +123,8 @@ const storeActions = { SAVE_LOGGING_OPTION: "saveLoggingOption", UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", + GET_VALID_IDEMPOTENCY_KEY: "getValidIdempotencyKey", + CORRECT_IDEMPOTENCY_KEY_EXPIRY: "correctIdempotencyKeyExpiry", }; export { storeActions }; diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 9b4b3719c..5c6c2e22d 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -98,6 +98,7 @@ const storeMutations = { UPDATE_LAST_PAGE_VISITED: "updateLastPageVisited", UPDATE_LOGGING_OPTION: "updateLoggingOption", UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", + UPDATE_IDEMPOTENCY_KEY: "updateIdempotencyKey", // EXPERIMENT MUTATIONS UPDATE_EXPERIMENTS: "updateExperiments", diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index adb0a1210..4a4936a7f 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -163,7 +163,7 @@ export default { console.log(`Price = ${this.amountDue}`); console.log(`Adyen Price = ${this.adyenPriceTotal}`); - const requestBody = this.getAdyenInitRequestInfo(); + const requestBody = await this.getAdyenInitRequestInfo(); console.log(`Calling with:`); console.log(requestBody); @@ -257,6 +257,7 @@ export default { this.dropinComponent = dropin; dropin.mount("#adyen-container"); + await this.dispatchStoreAction(storeActions.CORRECT_IDEMPOTENCY_KEY_EXPIRY, expiryTime); }, async handleCompletedPayment(result) { @@ -438,19 +439,13 @@ export default { }, // Now a Method so it is always freshly called and not cached. - getIdempotencyKey() { - const currentDateTime = new Date(); - const currentHour = currentDateTime.getUTCHours(); - const currentDate = currentDateTime.getUTCDate(); - const id = this?.$store?.getters?.order?.referralCorrelationId; - const system = this.sourceSystem; - const total = this.adyenPriceTotal; - - return `${id}-${system}-${currentDate}-${currentHour}-${total}`; + async getIdempotencyKey() { + return await this.dispatchStoreAction(storeActions.GET_VALID_IDEMPOTENCY_KEY); }, // Now a Method so it is always freshly called and not cached. - getAdyenInitRequestInfo() { + async getAdyenInitRequestInfo() { + const key = await this.getIdempotencyKey(); return { sourceSystem: this.sourceSystem, referralSequenceNumber: this.$store.getters.order.referralSequenceNumber, @@ -464,10 +459,9 @@ export default { stateOrProvince: this.locationInfo.state, returnUrl: applicationConfig.PIA_ADYEN_RETURN_URL, email: this.$store.getters.order.customer.emailAddress, - IP: "127.0.0.1", // TODO firstName: this.$store.getters.order.customer.firstName, lastName: this.$store.getters.order.customer.lastName, - idempotencyKey: this.getIdempotencyKey(), + idempotencyKey: key, }; }, }, diff --git a/src/store/index.js b/src/store/index.js index 10218892c..e740bf0d3 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -59,7 +59,7 @@ import { } from "@/helpers/recal-helper"; import { externalParameterStatus } from "@/constants/external-parameters"; import { experimentSettings } from "@/constants/experiments"; -import { addPricesToLineItems } from "@/helpers/pricing-helper"; +import { addPricesToLineItems, getAmountDue } from "@/helpers/pricing-helper"; // Export State const getDefaultState = () => { @@ -207,6 +207,12 @@ const getDefaultState = () => { loggingOption: false, hasAlreadyTriggeredError: false, }, + idempotencyKeyFields: { + referralCorrelationId: null, + totalInCents: 0, + expiryTime: null, + idempotencyKey: null, + }, }; }; @@ -862,6 +868,15 @@ export const mutations = { updateHasTriggeredError(state, hasTriggeredError) { state.applicationUser.hasAlreadyTriggeredError = hasTriggeredError; }, + updateIdempotencyKey( + state, + { idempotencyKey, referralCorrelationId, totalInCents, expiryTime } + ) { + state.idempotencyKeyFields.idempotencyKey = idempotencyKey; + state.idempotencyKeyFields.referralCorrelationId = referralCorrelationId; + state.idempotencyKeyFields.totalInCents = totalInCents; + state.idempotencyKeyFields.expiryTime = expiryTime; + }, }; // Export Getters @@ -1105,6 +1120,35 @@ export const getters = { state.order.customer.emailAddress ); }, + + isIdempotencyKeyValid: (state) => { + if (!state.idempotencyKeyFields?.idempotencyKey) { + return false; + } + + const referralCorrelationId = state.order?.referralCorrelationId; + if ( + !referralCorrelationId || + referralCorrelationId !== state.idempotencyKeyFields?.referralCorrelationId + ) { + return false; + } + + const total = getAmountDue(state.order?.lineItems); + const totalInCents = Math.round(total * 100); + if (totalInCents !== state.idempotencyKeyFields?.totalInCents) { + return false; + } + + const currentTime = new Date(); + const expiryTime = new Date(state.idempotencyKeyFields.expiryTime); + if (!expiryTime || expiryTime.getTime() < currentTime.getTime()) { + return false; + } + + // If all checks pass, then the key is valid. + return true; + }, }; // Export Actions @@ -3665,6 +3709,63 @@ export const actions = { updateHasTriggeredError(context, hasAlreadyTriggeredError) { context.commit(storeMutations.UPDATE_HAS_TRIGGERED_ERROR, hasAlreadyTriggeredError); }, + + getValidIdempotencyKey(context) { + if (context.getters.isIdempotencyKeyValid) { + return context.state.idempotencyKeyFields.idempotencyKey; + } + + // If invalid, need to regenerate. + const referralCorrelationId = context.state.order?.referralCorrelationId; + if (!referralCorrelationId) { + // Can't generate! + return null; + } + + const total = getAmountDue(context.state.order?.lineItems); + const totalInCents = Math.round(total * 100); + if (Number.isNaN(totalInCents)) { + // Can't generate! + return null; + } + + const currentTime = new Date(); + const nextHour = currentTime.getUTCHours() + 1; + currentTime.setUTCHours(nextHour); + + const newId = crypto.randomUUID(); + + const newKeyInfo = { + idempotencyKey: newId, + referralCorrelationId: referralCorrelationId, + totalInCents: totalInCents, + expiryTime: currentTime, + }; + + context.commit(storeMutations.UPDATE_IDEMPOTENCY_KEY, newKeyInfo); + + return newId; + }, + + correctIdempotencyKeyExpiry(context, expiryTime) { + if (!context.getters.isIdempotencyKeyValid) { + return; + } + + const currentExpiryTime = new Date(context.state.idempotencyKeyFields.expiryTime); + const expiryTimeAsDate = new Date(expiryTime); + + if (expiryTimeAsDate.getTime() < currentExpiryTime.getTime()) { + const existingKey = context.state.idempotencyKeyFields; + const newKey = { + referralCorrelationId: existingKey.referralCorrelationId, + totalInCents: existingKey.totalInCents, + idempotencyKey: existingKey.idempotencyKey, + expiryTime: expiryTimeAsDate, + }; + context.commit(storeMutations.UPDATE_IDEMPOTENCY_KEY, newKey); + } + }, }; export default createStore({