@7365admin1/module-hygiene 4.29.0 → 4.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@7365admin1/module-hygiene",
3
3
  "license": "MIT",
4
- "version": "4.29.0",
4
+ "version": "4.30.0",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -0,0 +1,160 @@
1
+ /*
2
+ * A PHOTO IS REQUIRED TO COMPLETE A TASK.
3
+ *
4
+ * Owner requirement, 2026-09-10: "once task was completed they required to take
5
+ * photo before completing the task ... all the services that has this module
6
+ * must be the same process."
7
+ *
8
+ * This endpoint is where every service and every client meets. The five field
9
+ * apps — M&E, Cleaning, Landscape, Pest, Pool — all PATCH
10
+ * `/api/hygiene-area-checklist/id/:id/set/:set/units/:unit/:decision` with a
11
+ * `serviceType`, and the web checklist posts here too. Enforcing it here is what
12
+ * makes "the same process" true, rather than five client guards that drift.
13
+ *
14
+ * The rule is exercised against the SHIPPED Joi schema, lifted out of the
15
+ * controller — not a re-typed copy, so a change to the real validation fails
16
+ * this file.
17
+ */
18
+ import test from "node:test";
19
+ import assert from "node:assert/strict";
20
+ import { readFileSync } from "node:fs";
21
+ import Joi from "joi";
22
+
23
+ const SRC = readFileSync(
24
+ new URL("../src/controllers/hygiene-area-checklist.controller.ts", import.meta.url),
25
+ "utf8",
26
+ );
27
+
28
+ /** The validation object from `updateAreaChecklistUnits`, evaluated for real. */
29
+ function schema() {
30
+ const fn = SRC.slice(SRC.indexOf("async function updateAreaChecklistUnits("));
31
+ const start = fn.indexOf("const validation = Joi.object({");
32
+ assert.notEqual(start, -1, "the validation block is gone");
33
+
34
+ // Walk braces from the opening `{` of Joi.object( ... ) to its match.
35
+ const open = fn.indexOf("{", start + "const validation = Joi.object(".length);
36
+ let depth = 0;
37
+ let end = -1;
38
+ for (let i = open; i < fn.length; i++) {
39
+ if (fn[i] === "{") depth++;
40
+ else if (fn[i] === "}") {
41
+ depth--;
42
+ if (depth === 0) {
43
+ end = i;
44
+ break;
45
+ }
46
+ }
47
+ }
48
+ assert.notEqual(end, -1, "could not read the validation block");
49
+
50
+ const body = fn
51
+ .slice(open, end + 1)
52
+ .replace(/\/\*[\s\S]*?\*\//g, "")
53
+ .replace(/\/\/[^\n]*/g, "");
54
+ return new Function("Joi", `return Joi.object(${body});`)(Joi);
55
+ }
56
+
57
+ const base = {
58
+ id: "6923d6664150ca6a69b9f2b2",
59
+ set: 1,
60
+ unit: "69251faf8c8936c9e6ebf095",
61
+ completedBy: "6925a31630abdbb211ecd9a8",
62
+ remarks: "done",
63
+ };
64
+
65
+ const validate = (payload) => schema().validate(payload);
66
+
67
+ /* ── completing requires evidence ──────────────────────────────────────── */
68
+
69
+ test("approving WITHOUT a photo is refused", () => {
70
+ const { error } = validate({ ...base, decision: "approve", approve: true });
71
+ assert.ok(error, "a task was completed with no photo");
72
+ assert.match(error.message, /photo is required/i);
73
+ });
74
+
75
+ test("an EMPTY attachment array is refused too", () => {
76
+ // This is the shape a client sends when its uploader failed — it satisfies
77
+ // `.optional()` while carrying no evidence at all.
78
+ const { error } = validate({
79
+ ...base,
80
+ decision: "approve",
81
+ approve: true,
82
+ attachment: [],
83
+ });
84
+ assert.ok(error, "an empty attachment list was accepted as evidence");
85
+ assert.match(error.message, /photo is required/i);
86
+ });
87
+
88
+ test("a BLANK id is not a photo", () => {
89
+ const { error } = validate({
90
+ ...base,
91
+ decision: "approve",
92
+ approve: true,
93
+ attachment: [""],
94
+ });
95
+ assert.ok(error, "a blank attachment id counted as a photo");
96
+ });
97
+
98
+ test("approving WITH a photo is accepted", () => {
99
+ const { error } = validate({
100
+ ...base,
101
+ decision: "approve",
102
+ approve: true,
103
+ attachment: ["68f05fbe0c11062938dadad9"],
104
+ });
105
+ assert.equal(error, undefined, error?.message);
106
+ });
107
+
108
+ test("MULTIPLE photos are accepted — that is the point", () => {
109
+ const { error } = validate({
110
+ ...base,
111
+ decision: "approve",
112
+ approve: true,
113
+ attachment: ["a1", "b2", "c3", "d4"],
114
+ });
115
+ assert.equal(error, undefined, error?.message);
116
+ });
117
+
118
+ /* ── rejecting must NOT be blocked ─────────────────────────────────────── */
119
+
120
+ test("REJECTING without a photo is still allowed", () => {
121
+ /*
122
+ * A reject is the technician reporting a problem. Demanding a photograph
123
+ * before somebody can report one would stop them reporting it.
124
+ */
125
+ const { error } = validate({ ...base, decision: "reject", reject: true });
126
+ assert.equal(error, undefined, error?.message);
127
+ });
128
+
129
+ test("rejecting WITH photos is still allowed", () => {
130
+ const { error } = validate({
131
+ ...base,
132
+ decision: "reject",
133
+ reject: true,
134
+ attachment: ["a1", "b2"],
135
+ });
136
+ assert.equal(error, undefined, error?.message);
137
+ });
138
+
139
+ /* ── controls ──────────────────────────────────────────────────────────── */
140
+
141
+ test("CONTROL: the other required fields still are", () => {
142
+ // Without this, the assertions above could pass against a schema that
143
+ // validates nothing.
144
+ for (const missing of ["id", "unit", "completedBy"]) {
145
+ const payload = {
146
+ ...base,
147
+ decision: "approve",
148
+ approve: true,
149
+ attachment: ["a1"],
150
+ };
151
+ delete payload[missing];
152
+ assert.ok(validate(payload).error, `${missing} is no longer required`);
153
+ }
154
+ });
155
+
156
+ test("CONTROL: the message names the action a person must take", () => {
157
+ // "Invalid payload" tells a technician standing in a plant room nothing.
158
+ const { error } = validate({ ...base, decision: "approve", approve: true });
159
+ assert.match(error.message, /take or upload at least one photo/i);
160
+ });