372 lines
11 KiB
Vue
372 lines
11 KiB
Vue
<script>
|
|
import axios from 'axios'
|
|
import { enqueueRequest } from "@/services/requestQueue.js";
|
|
import { buildCurrentReleaseHeaders, resolveReleaseApiUrl } from "@/services/releaseTimeline.js";
|
|
import { isTrustedReleaseUrl } from "@/services/releaseTrust.js";
|
|
|
|
/**
|
|
* Get the selected customer number for X-Customer-Number header (used by subusers)
|
|
* @returns {number|null}
|
|
*/
|
|
const getSelectedCustomerNumber = () => {
|
|
const stored = localStorage.getItem('selected_customer_number');
|
|
return stored ? parseInt(stored) : null;
|
|
};
|
|
|
|
const MY_ACTIVE_WASH_ENDPOINT = '/modules/self-serve/lane/wash/my-active-wash';
|
|
const ACTIVE_WASH_STARTED_STATUSES = new Set(['MACHINE_RELAY_ENABLED', 'MACHINE_STARTED']);
|
|
const SELF_SERVE_HARDWARE_QUEUE_GROUP = 'SELF_SERVE_HARDWARE';
|
|
const POS_SCANNER_QUEUE_GROUP = 'POS_SCANNER';
|
|
const POS_STRIPE_QUEUE_GROUP = 'POS_STRIPE';
|
|
const FETCH_TRANSPORT = 'fetch';
|
|
const SELF_SERVE_HARDWARE_ENDPOINTS = [
|
|
'/modules/self-serve/lane/command',
|
|
'/modules/self-serve/lane/relay/',
|
|
'/modules/self-serve/lane/gate/open',
|
|
'/modules/self-serve/lane/force/machine',
|
|
];
|
|
const POS_LATENCY_QUEUE_RULES = [
|
|
{
|
|
endpoints: ['/modules/scanner/lpr'],
|
|
queueGroup: POS_SCANNER_QUEUE_GROUP,
|
|
concurrencyLimit: 1,
|
|
retryByStatusCode: {},
|
|
skipRequestByteAccounting: true,
|
|
skipResponseByteAccounting: true,
|
|
skipNetworkTotals: true,
|
|
insightKey: 'scanner',
|
|
recordRecentOnSuccess: false,
|
|
trackActiveRequest: false,
|
|
trackProgressCounters: false,
|
|
},
|
|
{
|
|
endpoints: ['/modules/stripe/invoice'],
|
|
queueGroup: POS_STRIPE_QUEUE_GROUP,
|
|
concurrencyLimit: 2,
|
|
retryByStatusCode: {},
|
|
},
|
|
];
|
|
|
|
const normalizeStatus = (status) => String(status || '').trim().toUpperCase();
|
|
|
|
const parseDateTimeMs = (value) => {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
const timestamp = Date.parse(String(value));
|
|
return Number.isFinite(timestamp) ? timestamp : null;
|
|
};
|
|
|
|
const hasActiveWashStartEvidence = (details) => {
|
|
const session = details?.session || {};
|
|
const status = normalizeStatus(session?.status ?? details?.status);
|
|
|
|
return (
|
|
ACTIVE_WASH_STARTED_STATUSES.has(status) ||
|
|
session?.machine_relay_enabled === true ||
|
|
session?.machine_start_triggered === true ||
|
|
details?.machine_relay_enabled === true ||
|
|
details?.machine_start_triggered === true ||
|
|
parseDateTimeMs(session?.wash_started_at) !== null ||
|
|
parseDateTimeMs(session?.machine_start_triggered_at) !== null ||
|
|
parseDateTimeMs(session?.machine_relay_enabled_at) !== null
|
|
);
|
|
};
|
|
|
|
const normalizeActiveWashResponse = (url, method, response) => {
|
|
if (String(method || '').toUpperCase() !== 'GET' || !String(url || '').includes(MY_ACTIVE_WASH_ENDPOINT)) {
|
|
return response;
|
|
}
|
|
|
|
const details = response?.data?.data;
|
|
if (!details?.in_progress || hasActiveWashStartEvidence(details)) {
|
|
return response;
|
|
}
|
|
|
|
return {
|
|
...response,
|
|
data: {
|
|
...response.data,
|
|
data: {
|
|
...details,
|
|
in_progress: false,
|
|
},
|
|
},
|
|
};
|
|
};
|
|
|
|
const isSelfServeHardwareMutation = (url, method) => {
|
|
const normalizedMethod = String(method || '').trim().toUpperCase();
|
|
if (normalizedMethod === 'GET') {
|
|
return false;
|
|
}
|
|
|
|
const normalizedUrl = String(url || '');
|
|
return SELF_SERVE_HARDWARE_ENDPOINTS.some((endpoint) => normalizedUrl.includes(endpoint));
|
|
};
|
|
|
|
const findPosLatencyQueueRule = (url, method) => {
|
|
const normalizedMethod = String(method || '').trim().toUpperCase();
|
|
if (normalizedMethod === 'GET') {
|
|
return null;
|
|
}
|
|
|
|
const normalizedUrl = String(url || '');
|
|
return POS_LATENCY_QUEUE_RULES.find((rule) =>
|
|
rule.endpoints.some((endpoint) => normalizedUrl.includes(endpoint))
|
|
) || null;
|
|
};
|
|
|
|
const hasHeader = (headers, name) => {
|
|
const normalizedName = String(name || '').trim().toLowerCase();
|
|
if (!normalizedName || !headers || typeof headers !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
return Object.keys(headers).some((headerName) => String(headerName).toLowerCase() === normalizedName);
|
|
};
|
|
|
|
const parseFetchResponseData = async (response) => {
|
|
const text = await response.text();
|
|
if (!text) {
|
|
return null;
|
|
}
|
|
|
|
const contentType = response.headers?.get?.('content-type') || '';
|
|
if (
|
|
contentType.toLowerCase().includes('application/json') ||
|
|
text.trim().startsWith('{') ||
|
|
text.trim().startsWith('[')
|
|
) {
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch (_error) {
|
|
return text;
|
|
}
|
|
}
|
|
|
|
return text;
|
|
};
|
|
|
|
const buildFetchBody = (method, data, headers) => {
|
|
if (String(method || '').trim().toUpperCase() === 'GET' || data === undefined || data === null) {
|
|
return undefined;
|
|
}
|
|
|
|
if (
|
|
typeof Blob !== 'undefined' && data instanceof Blob ||
|
|
typeof FormData !== 'undefined' && data instanceof FormData ||
|
|
typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams ||
|
|
typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer ||
|
|
typeof ReadableStream !== 'undefined' && data instanceof ReadableStream ||
|
|
typeof data === 'string'
|
|
) {
|
|
return data;
|
|
}
|
|
|
|
if (!hasHeader(headers, 'Content-Type')) {
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
return JSON.stringify(data);
|
|
};
|
|
|
|
const executeFetchRequest = async ({ method, url, data, signal, headers }) => {
|
|
const fetchHeaders = { ...headers };
|
|
const body = buildFetchBody(method, data, fetchHeaders);
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: fetchHeaders,
|
|
...(body !== undefined ? { body } : {}),
|
|
...(signal ? { signal } : {}),
|
|
});
|
|
const responseData = await parseFetchResponseData(response);
|
|
const axiosLikeResponse = {
|
|
data: responseData,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: response.headers,
|
|
config: {
|
|
data,
|
|
headers: fetchHeaders,
|
|
method,
|
|
url,
|
|
},
|
|
request: null,
|
|
};
|
|
|
|
if (response.ok) {
|
|
return axiosLikeResponse;
|
|
}
|
|
|
|
const error = new Error(`Request failed with status code ${response.status}`);
|
|
error.name = 'AxiosError';
|
|
error.response = axiosLikeResponse;
|
|
throw error;
|
|
};
|
|
|
|
const buildRequestQueueOptions = (url, method, options = {}) => {
|
|
const queueOptions = {
|
|
retryByStatusCode: options?.retryByStatusCode,
|
|
shouldRetry: options?.shouldRetry,
|
|
queueGroup: options?.queueGroup,
|
|
concurrencyLimit: options?.concurrencyLimit,
|
|
skipRequestByteAccounting: options?.skipRequestByteAccounting,
|
|
skipResponseByteAccounting: options?.skipResponseByteAccounting,
|
|
skipNetworkTotals: options?.skipNetworkTotals,
|
|
insightKey: options?.insightKey,
|
|
recordRecentOnSuccess: options?.recordRecentOnSuccess,
|
|
trackActiveRequest: options?.trackActiveRequest,
|
|
trackProgressCounters: options?.trackProgressCounters,
|
|
};
|
|
|
|
if (isSelfServeHardwareMutation(url, method)) {
|
|
queueOptions.retryByStatusCode ??= {};
|
|
queueOptions.queueGroup ??= SELF_SERVE_HARDWARE_QUEUE_GROUP;
|
|
queueOptions.concurrencyLimit ??= 1;
|
|
}
|
|
|
|
const posQueueRule = findPosLatencyQueueRule(url, method);
|
|
if (posQueueRule) {
|
|
queueOptions.retryByStatusCode ??= posQueueRule.retryByStatusCode;
|
|
queueOptions.queueGroup ??= posQueueRule.queueGroup;
|
|
queueOptions.concurrencyLimit ??= posQueueRule.concurrencyLimit;
|
|
queueOptions.skipRequestByteAccounting ??= posQueueRule.skipRequestByteAccounting;
|
|
queueOptions.skipResponseByteAccounting ??= posQueueRule.skipResponseByteAccounting;
|
|
queueOptions.skipNetworkTotals ??= posQueueRule.skipNetworkTotals;
|
|
queueOptions.insightKey ??= posQueueRule.insightKey;
|
|
queueOptions.recordRecentOnSuccess ??= posQueueRule.recordRecentOnSuccess;
|
|
queueOptions.trackActiveRequest ??= posQueueRule.trackActiveRequest;
|
|
queueOptions.trackProgressCounters ??= posQueueRule.trackProgressCounters;
|
|
}
|
|
|
|
return queueOptions;
|
|
};
|
|
|
|
export const authenticatedRequest = (url, method, data, catchCallable = null, thenCallable = null, options = {}) => {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) {
|
|
//throw new Error('No token was found, unable to make authenticated request');
|
|
}
|
|
|
|
const requestUrl = resolveReleaseApiUrl(url);
|
|
const canSendCredentials = isTrustedReleaseUrl(requestUrl);
|
|
|
|
// Build headers
|
|
const headers = {
|
|
...buildCurrentReleaseHeaders(),
|
|
...(options?.headers || {}),
|
|
};
|
|
if (canSendCredentials && token && token.length > 0) {
|
|
headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
// Add X-Customer-Number header if subuser has selected a grant
|
|
const isSubuser = localStorage.getItem('is_subuser') === 'true';
|
|
const selectedCustomerNumber = getSelectedCustomerNumber();
|
|
if (canSendCredentials && isSubuser && selectedCustomerNumber) {
|
|
headers['X-Customer-Number'] = selectedCustomerNumber;
|
|
}
|
|
|
|
const useFetchTransport = options?.transport === FETCH_TRANSPORT;
|
|
|
|
return enqueueRequest(
|
|
() => useFetchTransport
|
|
? executeFetchRequest({
|
|
method,
|
|
url: requestUrl,
|
|
data,
|
|
signal: options?.signal,
|
|
headers,
|
|
})
|
|
: axios({
|
|
method,
|
|
url: requestUrl,
|
|
...(method === 'GET' ? { params: data } : { data }),
|
|
...(options?.signal ? { signal: options.signal } : {}),
|
|
__skipRequestQueue: true,
|
|
headers
|
|
}),
|
|
{
|
|
method,
|
|
url: requestUrl,
|
|
requestData: {
|
|
params: method === 'GET' ? data : null,
|
|
data: method === 'GET' ? null : data,
|
|
headers,
|
|
},
|
|
signal: options?.signal,
|
|
...buildRequestQueueOptions(requestUrl, method, options),
|
|
}
|
|
)
|
|
.catch((error) => {
|
|
if (catchCallable) {
|
|
// Call the catch callable
|
|
catchCallable(error);
|
|
}
|
|
throw error;
|
|
})
|
|
.then((response) => {
|
|
response = normalizeActiveWashResponse(url, method, response);
|
|
if (thenCallable) {
|
|
// Call the then callable
|
|
thenCallable(response);
|
|
}
|
|
return response;
|
|
});
|
|
};
|
|
|
|
export const unauthenticatedRequest = (url, method, data, catchCallable = null, thenCallable = null) => {
|
|
return axios({
|
|
method,
|
|
url: resolveReleaseApiUrl(url),
|
|
...(method === 'GET' ? { params: data } : { data }),
|
|
headers: buildCurrentReleaseHeaders(),
|
|
}).catch((error) => {
|
|
if (catchCallable) {
|
|
// Call the catch callable
|
|
catchCallable(error);
|
|
}
|
|
throw error;
|
|
}).then((response) => {
|
|
if (thenCallable) {
|
|
// Call the then callable
|
|
thenCallable(response);
|
|
}
|
|
return response;
|
|
});
|
|
};
|
|
|
|
export const paginatedGetRequest = (url, currentPage, itemsPerPage) => {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) {
|
|
return null;
|
|
}
|
|
const requestUrl = resolveReleaseApiUrl(url);
|
|
const canSendCredentials = isTrustedReleaseUrl(requestUrl);
|
|
|
|
// Build headers
|
|
const headers = {
|
|
...buildCurrentReleaseHeaders(),
|
|
};
|
|
if (canSendCredentials) {
|
|
headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
// Add X-Customer-Number header if subuser has selected a grant
|
|
const isSubuser = localStorage.getItem('is_subuser') === 'true';
|
|
const selectedCustomerNumber = getSelectedCustomerNumber();
|
|
if (canSendCredentials && isSubuser && selectedCustomerNumber) {
|
|
headers['X-Customer-Number'] = selectedCustomerNumber;
|
|
}
|
|
|
|
return axios.get(requestUrl, {
|
|
params: {
|
|
page: currentPage,
|
|
limit: itemsPerPage
|
|
},
|
|
headers
|
|
});
|
|
};
|
|
</script>
|