fix(ci): unblock Bird frontend release (#239)
Link Bird status translations to existing aliases and modernize the POS automatic-capture visual contract with deterministic one-time completion assertions.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref } from "vue";
|
||||
import Swal from "sweetalert2";
|
||||
import PayWithStripeButton from "@/components/displays/department/pos/displays/PayWithStripeButton.vue";
|
||||
import {
|
||||
@@ -22,6 +22,9 @@ import {
|
||||
} from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
||||
import { finalizeCurrentMobileOrder } from "@/components/displays/department/pos/steps/mobile/objects/mobileOrderCompletion.js";
|
||||
|
||||
const SUCCESS_RESET_DELAY_MS = 3000;
|
||||
const SUCCESS_RESET_DELAY_OVERRIDE_KEY = "__TW_POS_MOBILE_SUCCESS_RESET_DELAY_MS__";
|
||||
|
||||
const isCompleting = ref(false);
|
||||
const completionError = ref(null);
|
||||
const completedOrderId = ref(null);
|
||||
@@ -37,8 +40,12 @@ const normalizedDepartmentId = computed(() => {
|
||||
});
|
||||
|
||||
const currentPaymentIntent = computed(() => StripeModule.paymentIntents.paymentIntent.value);
|
||||
const paymentIntentState = computed(() => StripeModule.paymentIntents.getPaymentIntentState(currentPaymentIntent.value));
|
||||
const hasActivePaymentIntent = computed(() => StripeModule.paymentIntents.isPaymentIntentActive(currentPaymentIntent.value));
|
||||
const paymentIntentState = computed(() =>
|
||||
StripeModule.paymentIntents.getPaymentIntentState(currentPaymentIntent.value)
|
||||
);
|
||||
const hasActivePaymentIntent = computed(() =>
|
||||
StripeModule.paymentIntents.isPaymentIntentActive(currentPaymentIntent.value)
|
||||
);
|
||||
|
||||
const orderItemsTotal = computed(() => {
|
||||
return (Array.isArray(order_items.value) ? order_items.value : []).reduce((sum, item) => {
|
||||
@@ -56,18 +63,23 @@ const mobileOrderTotal = computed(() => {
|
||||
return orderItemsTotal.value;
|
||||
});
|
||||
|
||||
const orderReference = computed(() => metadata.getReference?.() || '');
|
||||
const orderNotes = computed(() => metadata.getNotes?.() || '');
|
||||
const orderRegistration = computed(() => reg_1.value || 'Unknown vehicle');
|
||||
const isBackDisabled = computed(() => isCompleting.value || paymentIntentState.value === 'succeeded');
|
||||
const orderReference = computed(() => metadata.getReference?.() || "");
|
||||
const orderNotes = computed(() => metadata.getNotes?.() || "");
|
||||
const orderRegistration = computed(() => reg_1.value || "Unknown vehicle");
|
||||
const isBackDisabled = computed(() => isCompleting.value || paymentIntentState.value === "succeeded");
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('da-DK', {
|
||||
style: 'currency',
|
||||
currency: 'DKK',
|
||||
return new Intl.NumberFormat("da-DK", {
|
||||
style: "currency",
|
||||
currency: "DKK",
|
||||
}).format(Number(amount || 0));
|
||||
};
|
||||
|
||||
const getSuccessResetDelayMs = () => {
|
||||
const override = Number(window[SUCCESS_RESET_DELAY_OVERRIDE_KEY]);
|
||||
return Number.isFinite(override) && override >= 0 ? override : SUCCESS_RESET_DELAY_MS;
|
||||
};
|
||||
|
||||
const navigateToItems = () => {
|
||||
setStep(2);
|
||||
if (normalizedOrderId.value) {
|
||||
@@ -83,7 +95,7 @@ const clearSuccessfulMobileCardFlow = () => {
|
||||
order_id.value = 0;
|
||||
popups.clear();
|
||||
resetPos();
|
||||
}, 3000);
|
||||
}, getSuccessResetDelayMs());
|
||||
};
|
||||
|
||||
const completeMobileCardPayment = async () => {
|
||||
@@ -100,21 +112,21 @@ const completeMobileCardPayment = async () => {
|
||||
|
||||
try {
|
||||
await finalizeCurrentMobileOrder();
|
||||
popups.select('completed_transaction', {
|
||||
popups.select("completed_transaction", {
|
||||
message: `Order #${normalizedOrderId.value} successfully created.`,
|
||||
});
|
||||
clearSuccessfulMobileCardFlow();
|
||||
} catch (error) {
|
||||
completedOrderId.value = null;
|
||||
completionError.value = SessionUser.functions.parseErrorMessage(error) || 'Unable to complete card payment.';
|
||||
console.error('Failed to complete mobile card payment:', error);
|
||||
completionError.value = SessionUser.functions.parseErrorMessage(error) || "Unable to complete card payment.";
|
||||
console.error("Failed to complete mobile card payment:", error);
|
||||
} finally {
|
||||
isCompleting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onBackClick = async () => {
|
||||
if (paymentIntentState.value === 'succeeded') {
|
||||
if (paymentIntentState.value === "succeeded") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -124,14 +136,14 @@ const onBackClick = async () => {
|
||||
}
|
||||
|
||||
const result = await Swal.fire({
|
||||
title: 'Payment in progress',
|
||||
text: 'Resume the current payment or cancel it before returning to the item step.',
|
||||
icon: 'warning',
|
||||
title: "Payment in progress",
|
||||
text: "Resume the current payment or cancel it before returning to the item step.",
|
||||
icon: "warning",
|
||||
showDenyButton: true,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Resume payment',
|
||||
denyButtonText: 'Cancel payment',
|
||||
cancelButtonText: 'Stay here',
|
||||
confirmButtonText: "Resume payment",
|
||||
denyButtonText: "Cancel payment",
|
||||
cancelButtonText: "Stay here",
|
||||
reverseButtons: true,
|
||||
});
|
||||
|
||||
@@ -144,7 +156,7 @@ const onBackClick = async () => {
|
||||
await StripeModule.paymentIntents.deletePaymentIntent(normalizedOrderId.value);
|
||||
navigateToItems();
|
||||
} catch (error) {
|
||||
completionError.value = SessionUser.functions.parseErrorMessage(error) || 'Unable to cancel the current payment.';
|
||||
completionError.value = SessionUser.functions.parseErrorMessage(error) || "Unable to cancel the current payment.";
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -171,9 +183,7 @@ const onBackClick = async () => {
|
||||
<div class="mobile-card-payment-view__summary box">
|
||||
<div class="mobile-card-payment-view__summary-row">
|
||||
<span>Order</span>
|
||||
<strong data-testid="pos-mobile-step-3-order-id">
|
||||
#{{ normalizedOrderId || 'Pending' }}
|
||||
</strong>
|
||||
<strong data-testid="pos-mobile-step-3-order-id"> #{{ normalizedOrderId || "Pending" }} </strong>
|
||||
</div>
|
||||
<div class="mobile-card-payment-view__summary-row">
|
||||
<span>Total</span>
|
||||
@@ -216,9 +226,7 @@ const onBackClick = async () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="mobile-card-payment-view__description">
|
||||
Use the selected card terminal for this order.
|
||||
</p>
|
||||
<p class="mobile-card-payment-view__description">Use the selected card terminal for this order.</p>
|
||||
|
||||
<PayWithStripeButton
|
||||
:department-id="normalizedDepartmentId"
|
||||
|
||||
@@ -1233,9 +1233,9 @@
|
||||
"server_url": "Server URL",
|
||||
"server_url_desc": "The Bird API base URL.",
|
||||
"state": {
|
||||
"degraded": "Degraded",
|
||||
"degraded": "@:cron.status.degraded",
|
||||
"disabled": "Disabled",
|
||||
"healthy": "Healthy",
|
||||
"healthy": "@:cron.status.healthy",
|
||||
"needs_configuration": "Needs configuration",
|
||||
"unavailable": "Unavailable"
|
||||
},
|
||||
|
||||
@@ -44,9 +44,9 @@
|
||||
"server_url": "Server URL",
|
||||
"server_url_desc": "The Bird API base URL.",
|
||||
"state": {
|
||||
"degraded": "Degraded",
|
||||
"degraded": "@:cron.status.degraded",
|
||||
"disabled": "Disabled",
|
||||
"healthy": "Healthy",
|
||||
"healthy": "@:cron.status.healthy",
|
||||
"needs_configuration": "Needs configuration",
|
||||
"unavailable": "Unavailable"
|
||||
},
|
||||
|
||||
@@ -1260,6 +1260,21 @@ test.describe("POS visuals", () => {
|
||||
test("mobile step 3 snapshot", async ({ page }, testInfo) => {
|
||||
test.skip(!testInfo.project.name.includes("mobile"), "Mobile only");
|
||||
|
||||
let captureRequestCount = 0;
|
||||
let completionRequestCount = 0;
|
||||
await page.addInitScript(() => {
|
||||
window.__TW_POS_MOBILE_SUCCESS_RESET_DELAY_MS__ = 60_000;
|
||||
});
|
||||
page.on("request", (request) => {
|
||||
const pathname = new URL(request.url()).pathname;
|
||||
if (request.method() === "POST" && pathname.endsWith("/orders/module/stripe/payment_intent/capture")) {
|
||||
captureRequestCount += 1;
|
||||
}
|
||||
if (request.method() === "POST" && pathname.endsWith("/orders/mark_as_completed")) {
|
||||
completionRequestCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: POS_PERMISSIONS,
|
||||
@@ -1304,6 +1319,14 @@ test.describe("POS visuals", () => {
|
||||
|
||||
await page.goto("/admin/12/modules/pos?id=54518&customer_id=999&step=3");
|
||||
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible();
|
||||
await expect(page.locator('[data-testid="pos-mobile-popup"][data-popup-id="completed_transaction"]')).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect.poll(() => captureRequestCount).toBe(1);
|
||||
await expect.poll(() => completionRequestCount).toBe(1);
|
||||
await page.waitForTimeout(500);
|
||||
expect(captureRequestCount).toBe(1);
|
||||
expect(completionRequestCount).toBe(1);
|
||||
await expect(page.getByTestId("pos-mobile-step-3")).toHaveScreenshot(
|
||||
"pos-mobile-step-3.png",
|
||||
MOBILE_PAYMENT_VISUAL_SNAPSHOT_OPTIONS
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 36 KiB |
Reference in New Issue
Block a user