## Summary
Closes (heuristic for) **TRU-99 / SENERE 7**: warn operators in the
customer portal wash flow when a customer is flagged as a red car.
## Detection rule — heuristic, Mads to refine
The canonical rule (plate scan vs. red-car tag) is still TBD by Mads. As
a sensible default that fits the existing data model, this PR uses a
**manual customer-attribute flag**:
- If a customer has one of the configurable attribute keys set, the
warning fires. Default keys: `isRedCar`, `is_red_car`, `redCar`,
`red_car`.
- Detection is **case-insensitive** and the key list is **configurable**
(callers can override) so the rule can be tightened later without
touching the UI.
- Detection lives in `src/composables/redCarDetector.js`; reactive
loading lives in `src/composables/useRedCarWarning.js`.
- The composable reuses the existing `/customer/attributes` endpoint via
`customerAttributeService.js` — no backend change needed.
## UI
- New `RedCarWarning.vue` component renders a dismissable Buefy warning
in the vehicle step of the self-serve flow, with title + reason + care
suggestion.
- Wired into `MyWashStart.vue` via the existing `VehicleInputSection` /
`SelfServeVehicleStep` props. The composable is called with the
effective customer number (authenticated subuser or typed-in).
- Translations added in all 5 locales: `da`, `en`, `sv`, `de`, `no`
(source + regenerated runtime files).
## Tests
- 22 detector unit tests (positive, negative, case-insensitive, custom
keys, dedupe, normalisation).
- 5 i18n key presence tests across all 5 locales.
- 4 `RedCarWarning` component tests (conditional render, dismiss
wiring).
All new + existing related tests pass: `vitest run` on
`red-car-detector`, `red-car-warning-i18n`, `red-car-warning`,
`my-wash-start`, `customer-rule-registry`, `customer-rule-tooltip` →
**84/84 green**.
## Out of scope / not touched
- Backend / API: reused existing `/customer/attributes` endpoint.
- `openclaw.json`, deployment, merge — not touched (per task
constraints).
- Customer-rule registry: not added to `CUSTOMER_RULE_DEFINITIONS`
because the red-car flag is a soft warning, not a product-restriction
rule. If Mads wants it surfaced in the customer rule manager UI, that is
a follow-up.
## Files
- `src/composables/redCarDetector.js` (new)
- `src/composables/useRedCarWarning.js` (new)
- `src/components/displays/selfServe/RedCarWarning.vue` (new)
- `src/components/displays/selfServe/SelfServeVehicleStep.vue` (prop +
render)
-
`src/views/dashboards/userDashboard/wash/components/VehicleInputSection.vue`
(prop pass-through)
- `src/views/dashboards/userDashboard/wash/MyWashStart.vue` (composable
+ prop binding)
- `src/i18n/source/{da,en,sv,de,no}/phrases/compat/self_wash/index.json`
(new keys)
- `src/i18n/generated/{da,en,sv,de,no}-v2.json` (regenerated)
- `tests/unit/red-car-detector.spec.js` (new)
- `tests/unit/red-car-warning-i18n.spec.js` (new)
- `tests/unit/red-car-warning.spec.js` (new)
Refs: TRU-99
---------
Co-authored-by: Jeppe B <jeppe@copenhagentruckwash.io>
Co-authored-by: Frontend Subagent <frontend-agent@openclaw.local>
Co-authored-by: Pleno Bugfix Bot <bugfix-bot@pleno.local>
75 lines
2.2 KiB
JavaScript
75 lines
2.2 KiB
JavaScript
// @vitest-environment jsdom
|
|
import { mount } from "@vue/test-utils";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import RedCarWarning from "@/components/displays/selfServe/RedCarWarning.vue";
|
|
import { createTestI18n } from "./helpers/mountWithApp.js";
|
|
|
|
const BMessageStub = {
|
|
name: "BMessage",
|
|
props: {
|
|
type: { type: String, default: "" },
|
|
title: { type: String, default: "" },
|
|
},
|
|
emits: ["close"],
|
|
template: `
|
|
<div
|
|
class="b-message-stub"
|
|
:data-type="type"
|
|
:data-title="title"
|
|
data-testid="red-car-warning"
|
|
>
|
|
<slot />
|
|
</div>
|
|
`,
|
|
};
|
|
|
|
const factory = (props = {}, messages = {}) => {
|
|
const i18n = createTestI18n({
|
|
en: {
|
|
self_wash: {
|
|
red_car_warning_title: "Red car on site",
|
|
red_car_warning_message: "Handle with extra care.",
|
|
red_car_warning_suggestion: "Use a gentler wash.",
|
|
...messages.en?.self_wash,
|
|
},
|
|
},
|
|
});
|
|
|
|
return mount(RedCarWarning, {
|
|
props,
|
|
global: {
|
|
plugins: [i18n],
|
|
stubs: {
|
|
BMessage: BMessageStub,
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
describe("RedCarWarning", () => {
|
|
it("renders the warning when isRedCar is true", () => {
|
|
const wrapper = factory({ isRedCar: true });
|
|
expect(wrapper.find('[data-testid="red-car-warning"]').exists()).toBe(true);
|
|
expect(wrapper.find('[data-testid="red-car-warning-message"]').text()).toBe("Handle with extra care.");
|
|
expect(wrapper.find('[data-testid="red-car-warning-suggestion"]').text()).toBe("Use a gentler wash.");
|
|
});
|
|
|
|
it("does not render the warning when isRedCar is false", () => {
|
|
const wrapper = factory({ isRedCar: false });
|
|
expect(wrapper.find('[data-testid="red-car-warning"]').exists()).toBe(false);
|
|
});
|
|
|
|
it("does not render by default (no prop passed)", () => {
|
|
const wrapper = factory();
|
|
expect(wrapper.find('[data-testid="red-car-warning"]').exists()).toBe(false);
|
|
});
|
|
|
|
it("forwards the close event from the b-message as a dismiss emit", () => {
|
|
const wrapper = factory({ isRedCar: true });
|
|
const bMessage = wrapper.findComponent(BMessageStub);
|
|
bMessage.vm.$emit("close");
|
|
expect(wrapper.emitted()).toHaveProperty("dismiss");
|
|
});
|
|
});
|