## 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>
151 lines
5.1 KiB
JavaScript
151 lines
5.1 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
DEFAULT_RED_CAR_ATTRIBUTE_KEYS,
|
|
extractAttributeKey,
|
|
extractAttributeKeys,
|
|
getRedCarAttributeKeys,
|
|
isRedCarCustomer,
|
|
} from "@/composables/redCarDetector.js";
|
|
|
|
describe("redCarDetector.extractAttributeKey", () => {
|
|
it("returns trimmed string for plain string entries", () => {
|
|
expect(extractAttributeKey("isRedCar")).toBe("isRedCar");
|
|
expect(extractAttributeKey(" invoiceAllOrdersIndividually ")).toBe("invoiceAllOrdersIndividually");
|
|
});
|
|
|
|
it("extracts the attribute field from object rows", () => {
|
|
expect(extractAttributeKey({ attribute: "isRedCar" })).toBe("isRedCar");
|
|
expect(extractAttributeKey({ attribute: "isRedCar", customer_number: 42 })).toBe("isRedCar");
|
|
});
|
|
|
|
it("falls back to key/name aliases when attribute is missing", () => {
|
|
expect(extractAttributeKey({ key: "redCar" })).toBe("redCar");
|
|
expect(extractAttributeKey({ name: "red_car" })).toBe("red_car");
|
|
});
|
|
|
|
it("returns empty string for invalid input", () => {
|
|
expect(extractAttributeKey(null)).toBe("");
|
|
expect(extractAttributeKey(undefined)).toBe("");
|
|
expect(extractAttributeKey(123)).toBe("");
|
|
expect(extractAttributeKey({})).toBe("");
|
|
expect(extractAttributeKey({ attribute: "" })).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("redCarDetector.extractAttributeKeys", () => {
|
|
it("returns the attribute key for each row", () => {
|
|
expect(extractAttributeKeys(["isRedCar", { attribute: "redCar" }, { key: "is_red_car" }])).toEqual([
|
|
"isRedCar",
|
|
"redCar",
|
|
"is_red_car",
|
|
]);
|
|
});
|
|
|
|
it("filters out invalid rows and empty keys", () => {
|
|
expect(
|
|
extractAttributeKeys([null, undefined, "isRedCar", "", { attribute: "" }, { attribute: " redCar " }])
|
|
).toEqual(["isRedCar", "redCar"]);
|
|
});
|
|
|
|
it("returns an empty array for non-array input", () => {
|
|
expect(extractAttributeKeys(null)).toEqual([]);
|
|
expect(extractAttributeKeys(undefined)).toEqual([]);
|
|
expect(extractAttributeKeys("isRedCar")).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("redCarDetector.isRedCarCustomer (positive cases)", () => {
|
|
it("flags a customer with the default isRedCar attribute", () => {
|
|
expect(isRedCarCustomer([{ attribute: "isRedCar" }])).toBe(true);
|
|
});
|
|
|
|
it("flags any of the default attribute keys", () => {
|
|
for (const key of DEFAULT_RED_CAR_ATTRIBUTE_KEYS) {
|
|
expect(isRedCarCustomer([{ attribute: key }])).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("flags when the attribute is a plain string", () => {
|
|
expect(isRedCarCustomer(["isRedCar"])).toBe(true);
|
|
expect(isRedCarCustomer(["red_car"])).toBe(true);
|
|
});
|
|
|
|
it("matches case-insensitively", () => {
|
|
expect(isRedCarCustomer([{ attribute: "ISREDCAR" }])).toBe(true);
|
|
expect(isRedCarCustomer([{ attribute: "isredcar" }])).toBe(true);
|
|
});
|
|
|
|
it("flags a customer with a custom configured key", () => {
|
|
expect(
|
|
isRedCarCustomer([{ attribute: "needsGentleWash" }], {
|
|
attributeKeys: ["needsGentleWash"],
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it("flags when at least one matching attribute is present among others", () => {
|
|
expect(
|
|
isRedCarCustomer([
|
|
{ attribute: "invoiceAllOrdersIndividually" },
|
|
{ attribute: "isRedCar" },
|
|
{ attribute: "onlyTankCleaning" },
|
|
])
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("redCarDetector.isRedCarCustomer (negative cases)", () => {
|
|
it("returns false for an empty attribute list", () => {
|
|
expect(isRedCarCustomer([])).toBe(false);
|
|
});
|
|
|
|
it("returns false when no attribute matches the default keys", () => {
|
|
expect(isRedCarCustomer([{ attribute: "onlyTankCleaning" }, { attribute: "invoiceAllOrdersIndividually" }])).toBe(
|
|
false
|
|
);
|
|
});
|
|
|
|
it("returns false for null/undefined input", () => {
|
|
expect(isRedCarCustomer(null)).toBe(false);
|
|
expect(isRedCarCustomer(undefined)).toBe(false);
|
|
});
|
|
|
|
it("returns false when configured keys are all empty strings", () => {
|
|
expect(isRedCarCustomer([{ attribute: "isRedCar" }], { attributeKeys: ["", " "] })).toBe(false);
|
|
});
|
|
|
|
it("returns false for an unrelated custom key when defaults are used", () => {
|
|
expect(isRedCarCustomer([{ attribute: "needsGentleWash" }])).toBe(false);
|
|
});
|
|
|
|
it("returns false for a custom key when a different custom key is configured", () => {
|
|
expect(
|
|
isRedCarCustomer([{ attribute: "needsGentleWash" }], {
|
|
attributeKeys: ["isRedCar"],
|
|
})
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("redCarDetector.getRedCarAttributeKeys", () => {
|
|
it("returns the default keys when no override is given", () => {
|
|
const keys = getRedCarAttributeKeys();
|
|
expect(keys).toContain("isredcar");
|
|
expect(keys).toContain("is_red_car");
|
|
expect(keys).toContain("redcar");
|
|
expect(keys).toContain("red_car");
|
|
});
|
|
|
|
it("normalizes and deduplicates custom keys", () => {
|
|
const keys = getRedCarAttributeKeys({
|
|
attributeKeys: [" isRedCar ", "ISREDCAR", "", "redCar"],
|
|
});
|
|
expect(keys).toEqual(["isredcar", "redcar"]);
|
|
});
|
|
|
|
it("returns an empty array when no valid keys are configured", () => {
|
|
expect(getRedCarAttributeKeys({ attributeKeys: ["", " "] })).toEqual([]);
|
|
});
|
|
});
|