## 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>
116 lines
3.2 KiB
JavaScript
116 lines
3.2 KiB
JavaScript
/**
|
|
* Red-car detection utilities for the customer portal.
|
|
*
|
|
* SENERE 7 / TRU-99: Until Mads defines the canonical rule for
|
|
* identifying a "red car" (e.g. plate scan, color tag, manual
|
|
* customer flag), we use a manual customer-attribute flag as the
|
|
* default heuristic. The attribute keys that trigger the warning
|
|
* are configurable so the rule can be tightened later without
|
|
* touching call sites.
|
|
*/
|
|
|
|
/**
|
|
* Default attribute keys that mark a customer as owning a red car.
|
|
* Multiple keys are supported to give staff a few options when
|
|
* tagging accounts in the customer-attribute editor.
|
|
*/
|
|
export const DEFAULT_RED_CAR_ATTRIBUTE_KEYS = Object.freeze([
|
|
"isRedCar",
|
|
"is_red_car",
|
|
"redCar",
|
|
"red_car",
|
|
]);
|
|
|
|
const normalizeKey = (key) => String(key ?? "").trim();
|
|
|
|
const toNormalizedKeySet = (keys) => {
|
|
const set = new Set();
|
|
if (!Array.isArray(keys)) {
|
|
return set;
|
|
}
|
|
for (const key of keys) {
|
|
const normalized = normalizeKey(key).toLowerCase();
|
|
if (normalized) {
|
|
set.add(normalized);
|
|
}
|
|
}
|
|
return set;
|
|
};
|
|
|
|
/**
|
|
* Extract the attribute key string from a single customer-attribute
|
|
* row. Accepts both the API shape (object with `attribute`) and the
|
|
* plain-string shorthand used in some call sites.
|
|
*
|
|
* @param {string|object|undefined|null} entry
|
|
* @returns {string}
|
|
*/
|
|
export const extractAttributeKey = (entry) => {
|
|
if (typeof entry === "string") {
|
|
return normalizeKey(entry);
|
|
}
|
|
if (entry && typeof entry === "object") {
|
|
const candidate = entry.attribute ?? entry.key ?? entry.name;
|
|
return normalizeKey(candidate);
|
|
}
|
|
return "";
|
|
};
|
|
|
|
/**
|
|
* Extract the attribute key strings from a list of customer
|
|
* attributes. Returns an empty array for invalid input.
|
|
*
|
|
* @param {Array<string|object|undefined|null>|null|undefined} attributes
|
|
* @returns {string[]}
|
|
*/
|
|
export const extractAttributeKeys = (attributes) => {
|
|
if (!Array.isArray(attributes)) {
|
|
return [];
|
|
}
|
|
const keys = [];
|
|
for (const entry of attributes) {
|
|
const key = extractAttributeKey(entry);
|
|
if (key) {
|
|
keys.push(key);
|
|
}
|
|
}
|
|
return keys;
|
|
};
|
|
|
|
/**
|
|
* Determine whether a customer should be flagged as a "red car"
|
|
* based on their attribute list. The check is case-insensitive.
|
|
*
|
|
* @param {Array<string|object|undefined|null>|null|undefined} attributes
|
|
* @param {object} [options]
|
|
* @param {string[]} [options.attributeKeys] - attribute keys that
|
|
* mark a red car. Defaults to DEFAULT_RED_CAR_ATTRIBUTE_KEYS.
|
|
* @returns {boolean}
|
|
*/
|
|
export const isRedCarCustomer = (attributes, options = {}) => {
|
|
const keys = toNormalizedKeySet(
|
|
options.attributeKeys ?? DEFAULT_RED_CAR_ATTRIBUTE_KEYS
|
|
);
|
|
if (keys.size === 0) {
|
|
return false;
|
|
}
|
|
const attributeKeys = extractAttributeKeys(attributes);
|
|
return attributeKeys.some((key) => keys.has(key.toLowerCase()));
|
|
};
|
|
|
|
/**
|
|
* Return the configured red-car attribute keys (normalized,
|
|
* deduplicated, lowercased). Useful for displaying the active
|
|
* heuristic to operators.
|
|
*
|
|
* @param {object} [options]
|
|
* @param {string[]} [options.attributeKeys]
|
|
* @returns {string[]}
|
|
*/
|
|
export const getRedCarAttributeKeys = (options = {}) => {
|
|
const keys = toNormalizedKeySet(
|
|
options.attributeKeys ?? DEFAULT_RED_CAR_ATTRIBUTE_KEYS
|
|
);
|
|
return Array.from(keys);
|
|
};
|