Planning merge conflict resolution
This commit is contained in:
@@ -16,8 +16,8 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
format-tests:
|
||||
# Match the labels exposed by the Coolify-managed GitHub runner.
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
# Pull requests run untrusted code, so use ephemeral GitHub-hosted runners.
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
build-and-unit:
|
||||
needs: format-tests
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
e2e-pr:
|
||||
if: github.event_name != 'schedule'
|
||||
needs: build-and-unit
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch
|
||||
needs: build-and-unit
|
||||
name: E2E-full-${{ matrix.role }}-${{ matrix.browser_label }}-${{ matrix.device }}
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
selectPreferredStripeTerminalReaderId,
|
||||
STRIPE_TERMINAL_STATUS,
|
||||
} from "@/components/displays/department/pos/displays/stripeTerminalReaders.js";
|
||||
import { normalizeStripeInvoice } from "@/components/displays/department/pos/displays/stripeEmailInvoice.js";
|
||||
|
||||
const POLLING_INTERVAL_MS = 5000;
|
||||
const STRIPE_TERMINAL_SETUP_REQUIRED_CODE = 'stripe_terminal_setup_required';
|
||||
@@ -87,29 +88,6 @@ const selectedTaxRate = ref(1);
|
||||
const paymentIntent = computed(() => StripeModule.paymentIntents.paymentIntent.value);
|
||||
const isTerminalPaymentCaptured = computed(() => StripeModule.paymentIntents.isPaymentIntentAmountReceived(paymentIntent.value));
|
||||
|
||||
const normalizeStripeInvoice = (value) => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const invoiceId = value.invoice_id || value.id || null;
|
||||
if (!invoiceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: value.id ?? props.order_id,
|
||||
invoice_id: invoiceId,
|
||||
customer_id: value.customer_id ?? null,
|
||||
url: value.url || value.hosted_invoice_url || null,
|
||||
created_at: value.created_at || null,
|
||||
paid: Boolean(value.paid),
|
||||
status: value.status || 'unknown',
|
||||
amount_due: Number(value.amount_due ?? 0),
|
||||
amount_paid: Number(value.amount_paid ?? 0),
|
||||
};
|
||||
};
|
||||
|
||||
const hasStripeEmailInvoice = computed(() => stripeInvoice.value !== null);
|
||||
const isStripeEmailInvoicePaid = computed(() => stripeInvoice.value?.paid === true);
|
||||
const isStripeEmailInvoiceTerminalState = computed(() => {
|
||||
@@ -202,7 +180,7 @@ const loadStripeInvoiceState = async () => {
|
||||
|
||||
try {
|
||||
const response = await getOrder(props.order_id, true);
|
||||
const nextInvoice = normalizeStripeInvoice(response?.data?.includes?.stripeModuleOrders);
|
||||
const nextInvoice = normalizeStripeInvoice(response?.data?.includes?.stripeModuleOrders, props.order_id);
|
||||
stripeInvoice.value = nextInvoice;
|
||||
if (nextInvoice) {
|
||||
emailPanelState.value = 'tracking';
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
const STRIPE_INVOICE_PAID_STATUS = 'paid';
|
||||
|
||||
export const parseStripeInvoicePaidFlag = (value) => {
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return value === 1;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const normalizedValue = value.trim().toLowerCase();
|
||||
if (['true', '1'].includes(normalizedValue)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (['false', '0', ''].includes(normalizedValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const toFiniteNumber = (value, fallback = 0) => {
|
||||
const parsedValue = Number(value ?? fallback);
|
||||
return Number.isFinite(parsedValue) ? parsedValue : fallback;
|
||||
};
|
||||
|
||||
export const normalizeStripeInvoice = (value, fallbackOrderId = null) => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const invoiceId = value.invoice_id || value.id || null;
|
||||
if (!invoiceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const status = String(value.status || 'unknown').toLowerCase();
|
||||
const amountDue = toFiniteNumber(value.amount_due);
|
||||
const amountPaid = toFiniteNumber(value.amount_paid);
|
||||
const hasCoveredAmountDue = amountDue <= 0 || amountPaid >= amountDue;
|
||||
const isPaid = status === STRIPE_INVOICE_PAID_STATUS
|
||||
&& parseStripeInvoicePaidFlag(value.paid)
|
||||
&& hasCoveredAmountDue;
|
||||
|
||||
return {
|
||||
id: value.id ?? fallbackOrderId,
|
||||
invoice_id: invoiceId,
|
||||
customer_id: value.customer_id ?? null,
|
||||
url: value.url || value.hosted_invoice_url || null,
|
||||
created_at: value.created_at || null,
|
||||
paid: isPaid,
|
||||
status,
|
||||
amount_due: amountDue,
|
||||
amount_paid: amountPaid,
|
||||
};
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import {
|
||||
cancelEdgeGatewayOperation,
|
||||
clearEdgeGatewayWorkspaceCache,
|
||||
createEdgeGatewayInstallToken,
|
||||
createEdgeGatewayOperation,
|
||||
deleteEdgeGateway,
|
||||
@@ -12,10 +13,12 @@ import {
|
||||
getEdgeGatewayStatistics,
|
||||
getEdgeGatewayTasks,
|
||||
listEdgeGatewayDepartments,
|
||||
isEdgeGatewayAuthorizationError,
|
||||
listEdgeGateways,
|
||||
peekCachedEdgeGateway,
|
||||
peekCachedEdgeGatewayDepartments,
|
||||
peekCachedEdgeGatewayList,
|
||||
removeEdgeGatewayCache,
|
||||
rotateEdgeGatewayCredentials,
|
||||
saveEdgeGatewayBindings,
|
||||
setDepartmentGatewayCutover,
|
||||
@@ -1215,6 +1218,15 @@ const refreshSelected = async (gatewayId = activeGatewayId.value, { forceRefresh
|
||||
}
|
||||
return gateway;
|
||||
} catch (error) {
|
||||
if (isEdgeGatewayAuthorizationError(error)) {
|
||||
removeEdgeGatewayCache(gatewayId);
|
||||
unavailableGatewayId.value = String(gatewayId);
|
||||
selectedGateway.value = null;
|
||||
resetGatewayViewSnapshots();
|
||||
fail(error);
|
||||
return null;
|
||||
}
|
||||
|
||||
const fallbackGateway = findLocalGatewaySnapshot(gatewayId);
|
||||
if (fallbackGateway) {
|
||||
setSelectedGatewaySnapshot(fallbackGateway);
|
||||
@@ -1561,6 +1573,16 @@ const load = async () => {
|
||||
departments.value = unwrap(departmentsResponse, []);
|
||||
await syncSelection();
|
||||
} catch (error) {
|
||||
if (isEdgeGatewayAuthorizationError(error)) {
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
gateways.value = [];
|
||||
fleetUsage.value = buildFleetUsageFromRows([]);
|
||||
if (activeGatewayId.value) {
|
||||
unavailableGatewayId.value = String(activeGatewayId.value);
|
||||
}
|
||||
selectedGateway.value = null;
|
||||
resetGatewayViewSnapshots();
|
||||
}
|
||||
fail(error);
|
||||
} finally {
|
||||
loading.value.init = false;
|
||||
|
||||
@@ -10,6 +10,7 @@ const listRequestsInFlight = new Map();
|
||||
const detailRequestsInFlight = new Map();
|
||||
let departmentsRequestInFlight = null;
|
||||
let edgeGatewayWorkspaceCache = null;
|
||||
let edgeGatewayWorkspaceCacheStorageKey = null;
|
||||
|
||||
const cloneJson = (value) => (value === null || value === undefined ? value : JSON.parse(JSON.stringify(value)));
|
||||
const normalizeDepartmentId = (departmentId) =>
|
||||
@@ -132,6 +133,7 @@ const getStoredValue = (key) => {
|
||||
}
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
const hashScopePart = (value) => {
|
||||
let hash = 0;
|
||||
const input = String(value || "");
|
||||
@@ -212,6 +214,48 @@ const loadWorkspaceCache = () => {
|
||||
return edgeGatewayWorkspaceCache;
|
||||
}
|
||||
|
||||
=======
|
||||
const scopedWorkspaceCacheKey = () => {
|
||||
const localStorageHandle = storage();
|
||||
if (!localStorageHandle) {
|
||||
return EDGE_GATEWAY_WORKSPACE_CACHE_KEY;
|
||||
}
|
||||
|
||||
const sessionParts = [
|
||||
localStorageHandle.getItem("token") || "anonymous",
|
||||
localStorageHandle.getItem("is_subuser") === "true" ? "subuser" : "primary",
|
||||
localStorageHandle.getItem("selected_customer_number") || "none",
|
||||
];
|
||||
const principal = sessionParts.join("|");
|
||||
let hash = 0;
|
||||
for (let index = 0; index < principal.length; index += 1) {
|
||||
hash = (hash * 31 + principal.charCodeAt(index)) >>> 0;
|
||||
}
|
||||
|
||||
return `${EDGE_GATEWAY_WORKSPACE_CACHE_KEY}.${hash.toString(36)}`;
|
||||
};
|
||||
|
||||
const resetWorkspaceCacheMemory = (storageKey = scopedWorkspaceCacheKey()) => {
|
||||
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache();
|
||||
edgeGatewayWorkspaceCacheStorageKey = storageKey;
|
||||
return edgeGatewayWorkspaceCache;
|
||||
};
|
||||
|
||||
const loadWorkspaceCache = () => {
|
||||
const storageKey = scopedWorkspaceCacheKey();
|
||||
if (edgeGatewayWorkspaceCache !== null && edgeGatewayWorkspaceCacheStorageKey === storageKey) {
|
||||
return edgeGatewayWorkspaceCache;
|
||||
}
|
||||
|
||||
const localStorageHandle = storage();
|
||||
if (!localStorageHandle) {
|
||||
return resetWorkspaceCacheMemory(storageKey);
|
||||
}
|
||||
|
||||
try {
|
||||
localStorageHandle.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
|
||||
const decoded = JSON.parse(localStorageHandle.getItem(storageKey) || "null");
|
||||
>>>>>>> refs/remotes/origin/master
|
||||
edgeGatewayWorkspaceCache = {
|
||||
...createEmptyWorkspaceCache(scope),
|
||||
cachedAt: Number(decoded.cachedAt || Date.now()),
|
||||
@@ -225,9 +269,14 @@ const loadWorkspaceCache = () => {
|
||||
? Object.fromEntries(Object.entries(decoded.details).filter(([, entry]) => isFreshCacheEntry(entry)))
|
||||
: {},
|
||||
};
|
||||
edgeGatewayWorkspaceCacheStorageKey = storageKey;
|
||||
} catch (_error) {
|
||||
<<<<<<< HEAD
|
||||
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache(scope);
|
||||
storageHandle.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
|
||||
=======
|
||||
resetWorkspaceCacheMemory(storageKey);
|
||||
>>>>>>> refs/remotes/origin/master
|
||||
}
|
||||
|
||||
return edgeGatewayWorkspaceCache;
|
||||
@@ -240,6 +289,7 @@ const persistWorkspaceCache = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
edgeGatewayWorkspaceCache.cachedAt = Date.now();
|
||||
storageHandle.setItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY, JSON.stringify(edgeGatewayWorkspaceCache));
|
||||
};
|
||||
@@ -253,6 +303,11 @@ export const clearEdgeGatewayWorkspaceCache = () => {
|
||||
}
|
||||
|
||||
storageHandle.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
|
||||
=======
|
||||
const storageKey = edgeGatewayWorkspaceCacheStorageKey || scopedWorkspaceCacheKey();
|
||||
localStorageHandle.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
|
||||
localStorageHandle.setItem(storageKey, JSON.stringify(edgeGatewayWorkspaceCache));
|
||||
>>>>>>> refs/remotes/origin/master
|
||||
};
|
||||
|
||||
const mergeGatewaySnapshots = (currentGateway, nextGateway) => {
|
||||
@@ -412,6 +467,27 @@ const buildOperationPayload = (typeOrPayload, request = {}) => {
|
||||
export const unwrapEdgeGatewayResponse = (response, fallback = null) => response?.data?.data ?? fallback;
|
||||
export const unwrapEdgeGatewayMeta = (response) => response?.data?.meta ?? {};
|
||||
|
||||
export const isEdgeGatewayAuthorizationError = (error) => {
|
||||
const status = Number(error?.response?.status || error?.status || error?.response?.data?.status || 0);
|
||||
return status === 401 || status === 403;
|
||||
};
|
||||
|
||||
export const clearEdgeGatewayWorkspaceCache = () => {
|
||||
const localStorageHandle = storage();
|
||||
const storageKey = scopedWorkspaceCacheKey();
|
||||
resetWorkspaceCacheMemory(storageKey);
|
||||
if (!localStorageHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(localStorageHandle).forEach((key) => {
|
||||
if (key === EDGE_GATEWAY_WORKSPACE_CACHE_KEY || key.startsWith(`${EDGE_GATEWAY_WORKSPACE_CACHE_KEY}.`)) {
|
||||
localStorageHandle.removeItem(key);
|
||||
}
|
||||
});
|
||||
localStorageHandle.removeItem(storageKey);
|
||||
};
|
||||
|
||||
export const peekCachedEdgeGatewayDepartments = () => {
|
||||
const entry = loadWorkspaceCache().departments;
|
||||
const departments = entry?.data;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// @vitest-environment jsdom
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
>>>>>>> refs/remotes/origin/master
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -12,21 +15,31 @@ vi.mock("@/components/session/authenticatedRequest.vue", () => ({
|
||||
|
||||
import {
|
||||
EDGE_GATEWAY_WORKSPACE_CACHE_KEY,
|
||||
<<<<<<< HEAD
|
||||
EDGE_GATEWAY_WORKSPACE_CACHE_TTL_MS,
|
||||
clearEdgeGatewayWorkspaceCache,
|
||||
=======
|
||||
clearEdgeGatewayWorkspaceCache,
|
||||
getEdgeGateway,
|
||||
>>>>>>> refs/remotes/origin/master
|
||||
getEdgeGatewayInstallTokenStatus,
|
||||
listEdgeGateways,
|
||||
peekCachedEdgeGateway,
|
||||
peekCachedEdgeGatewayList,
|
||||
getEdgeGatewayModuleConfig,
|
||||
peekCachedEdgeGateway,
|
||||
setEdgeGatewayModuleConfig,
|
||||
} from "@/services/edgeGateways.js";
|
||||
|
||||
describe("edge gateway service", () => {
|
||||
beforeEach(() => {
|
||||
authenticatedRequestMock.mockReset();
|
||||
<<<<<<< HEAD
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
=======
|
||||
window.localStorage.clear();
|
||||
>>>>>>> refs/remotes/origin/master
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
});
|
||||
|
||||
@@ -86,7 +99,27 @@ describe("edge gateway service", () => {
|
||||
expect(authenticatedRequestMock).toHaveBeenCalledWith("/edge-gateways/install-token/9001/status", "GET", {});
|
||||
});
|
||||
|
||||
<<<<<<< HEAD
|
||||
it("uses a stable browser cache namespace for session-scoped workspace snapshots", () => {
|
||||
=======
|
||||
it("scopes workspace snapshots to the active session principal", async () => {
|
||||
window.localStorage.setItem("token", "user-one-token");
|
||||
authenticatedRequestMock.mockResolvedValueOnce({
|
||||
data: {
|
||||
data: { id: 101, label: "User One Gateway" },
|
||||
},
|
||||
});
|
||||
|
||||
await getEdgeGateway(101);
|
||||
expect(peekCachedEdgeGateway(101)?.label).toBe("User One Gateway");
|
||||
|
||||
window.localStorage.setItem("token", "user-two-token");
|
||||
expect(peekCachedEdgeGateway(101)).toBeNull();
|
||||
expect(window.localStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps a stable browser cache namespace for workspace snapshots", () => {
|
||||
>>>>>>> refs/remotes/origin/master
|
||||
const source = readFileSync(join(process.cwd(), "src/services/edgeGateways.js"), "utf8");
|
||||
|
||||
expect(EDGE_GATEWAY_WORKSPACE_CACHE_KEY).toBe("truckwash.edgeGatewayWorkspace.cache.v1");
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
normalizeStripeInvoice,
|
||||
parseStripeInvoicePaidFlag,
|
||||
} from "@/components/displays/department/pos/displays/stripeEmailInvoice.js";
|
||||
|
||||
describe("stripeEmailInvoice", () => {
|
||||
it("parses invoice paid flags without treating non-empty unpaid strings as paid", () => {
|
||||
expect(parseStripeInvoicePaidFlag(true)).toBe(true);
|
||||
expect(parseStripeInvoicePaidFlag(1)).toBe(true);
|
||||
expect(parseStripeInvoicePaidFlag("true")).toBe(true);
|
||||
expect(parseStripeInvoicePaidFlag("1")).toBe(true);
|
||||
|
||||
expect(parseStripeInvoicePaidFlag(false)).toBe(false);
|
||||
expect(parseStripeInvoicePaidFlag(0)).toBe(false);
|
||||
expect(parseStripeInvoicePaidFlag("false")).toBe(false);
|
||||
expect(parseStripeInvoicePaidFlag("0")).toBe(false);
|
||||
expect(parseStripeInvoicePaidFlag("open")).toBe(false);
|
||||
});
|
||||
|
||||
it("requires paid status, a paid flag, and covered amount before unlocking invoice completion", () => {
|
||||
expect(
|
||||
normalizeStripeInvoice({
|
||||
id: "in_unpaid_string",
|
||||
paid: "false",
|
||||
status: "open",
|
||||
amount_due: "1000",
|
||||
amount_paid: "0",
|
||||
})?.paid
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
normalizeStripeInvoice({
|
||||
id: "in_open_true_flag",
|
||||
paid: true,
|
||||
status: "open",
|
||||
amount_due: "1000",
|
||||
amount_paid: "1000",
|
||||
})?.paid
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
normalizeStripeInvoice({
|
||||
id: "in_underpaid",
|
||||
paid: true,
|
||||
status: "paid",
|
||||
amount_due: "1000",
|
||||
amount_paid: "999",
|
||||
})?.paid
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
normalizeStripeInvoice({
|
||||
id: "in_paid",
|
||||
paid: "true",
|
||||
status: "paid",
|
||||
amount_due: "1000",
|
||||
amount_paid: "1000",
|
||||
})?.paid
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user