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:
Jeppe B
2026-07-29 21:22:34 +02:00
committed by GitHub
parent 468d436d3e
commit 14a0d65a01
7 changed files with 63 additions and 32 deletions
@@ -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"
+2 -2
View File
@@ -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"
},
+23
View File
@@ -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