@defra/grants-reporting-publisher 0.0.3 → 0.1.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/CHANGELOG.md +6 -0
- package/package.json +3 -2
- package/src/constants.js +2 -0
- package/src/index.js +1 -0
- package/src/publish.test.js +6 -1
- package/src/schema.js +43 -9
- package/src/validate.test.js +215 -7
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/grants-reporting-publisher",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "NPM module for publishing grants reporting events",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
".": "./src/index.js",
|
|
33
33
|
"./publish": "./src/publish.js",
|
|
34
34
|
"./validate": "./src/validate.js",
|
|
35
|
-
"./schema": "./src/schema.js"
|
|
35
|
+
"./schema": "./src/schema.js",
|
|
36
|
+
"./constants": "./src/constants.js"
|
|
36
37
|
},
|
|
37
38
|
"license": "OGL-UK-3.0",
|
|
38
39
|
"bugs": {
|
package/src/constants.js
ADDED
package/src/index.js
CHANGED
package/src/publish.test.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { publishReportingEvent } from "./publish.js";
|
|
2
|
+
import { AGREEMENT_STATUS_CHANGED } from "./constants.js";
|
|
2
3
|
|
|
3
4
|
const mockSend = vi.hoisted(() =>
|
|
4
5
|
vi.fn().mockResolvedValue({ MessageId: "msg-001" }),
|
|
@@ -22,8 +23,12 @@ const validEvent = {
|
|
|
22
23
|
datetime: "2025-12-01T12:51:41.381Z",
|
|
23
24
|
version: "1.0.0",
|
|
24
25
|
application: "FCP001",
|
|
26
|
+
service: "grants",
|
|
25
27
|
eventData: {
|
|
26
|
-
|
|
28
|
+
eventType: AGREEMENT_STATUS_CHANGED,
|
|
29
|
+
agreementId: "WMP123456789",
|
|
30
|
+
agreementStatus: "created",
|
|
31
|
+
statusDate: "2025-12-01T12:51:41.381Z",
|
|
27
32
|
},
|
|
28
33
|
};
|
|
29
34
|
|
package/src/schema.js
CHANGED
|
@@ -1,28 +1,62 @@
|
|
|
1
1
|
import Joi from "joi";
|
|
2
|
+
import { AGREEMENT_CREATED, AGREEMENT_STATUS_CHANGED } from "./constants.js";
|
|
2
3
|
|
|
3
4
|
const MAX_30 = 30;
|
|
4
5
|
const MAX_50 = 50;
|
|
5
|
-
const MAX_120 = 120;
|
|
6
6
|
|
|
7
7
|
const accountId = Joi.alternatives()
|
|
8
8
|
.try(Joi.string().max(MAX_50), Joi.number())
|
|
9
9
|
.custom(String);
|
|
10
10
|
|
|
11
11
|
export const eventSchema = Joi.object({
|
|
12
|
-
user: Joi.string().max(MAX_50).allow(""),
|
|
13
|
-
sessionId: Joi.string().max(MAX_50).allow(""),
|
|
14
12
|
correlationId: Joi.string().max(MAX_50).required(),
|
|
15
13
|
datetime: Joi.date().iso().required(),
|
|
16
14
|
version: Joi.string().max(10).required(),
|
|
17
15
|
application: Joi.string().max(MAX_30).required(),
|
|
18
16
|
service: Joi.string().max(MAX_30).required(),
|
|
19
17
|
eventData: Joi.object({
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
eventType: Joi.string()
|
|
19
|
+
.valid(AGREEMENT_CREATED, AGREEMENT_STATUS_CHANGED)
|
|
20
|
+
.required(),
|
|
21
|
+
})
|
|
22
|
+
.when(Joi.object({ eventType: AGREEMENT_CREATED }).unknown(), {
|
|
23
|
+
then: Joi.object({
|
|
24
|
+
agreementId: Joi.string().required(),
|
|
25
|
+
agreementType: Joi.string().required(),
|
|
26
|
+
agreementStatus: Joi.string().required(),
|
|
27
|
+
agreementStartDate: Joi.date().iso().optional(),
|
|
28
|
+
agreementEndDate: Joi.date().iso().optional(),
|
|
29
|
+
agreementValue: Joi.number().optional(),
|
|
30
|
+
sbi: accountId.required(),
|
|
31
|
+
options: Joi.array()
|
|
32
|
+
.items(
|
|
33
|
+
Joi.object({
|
|
34
|
+
parcelReference: Joi.string().allow("").required(),
|
|
35
|
+
parcelSizeUnderAgreement: Joi.number().optional(),
|
|
36
|
+
optionCode: Joi.string().required(),
|
|
37
|
+
optionYear: Joi.number().integer().optional(),
|
|
38
|
+
optionStartDate: Joi.date().iso().required(),
|
|
39
|
+
optionEndDate: Joi.date().iso().required(),
|
|
40
|
+
optionQuantity: Joi.number().required(),
|
|
41
|
+
optionValue: Joi.number().required(),
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
.required(),
|
|
45
|
+
}),
|
|
46
|
+
})
|
|
47
|
+
.when(Joi.object({ eventType: AGREEMENT_STATUS_CHANGED }).unknown(), {
|
|
48
|
+
then: Joi.object({
|
|
49
|
+
agreementId: Joi.string().required(),
|
|
50
|
+
agreementStatus: Joi.string().required(),
|
|
51
|
+
statusDate: Joi.date().iso().required(),
|
|
52
|
+
agreementStartDate: Joi.date().iso().optional(),
|
|
53
|
+
agreementEndDate: Joi.date().iso().optional(),
|
|
54
|
+
agreementValue: Joi.number().optional(),
|
|
55
|
+
userId: Joi.string().optional(),
|
|
56
|
+
}),
|
|
57
|
+
})
|
|
58
|
+
.unknown()
|
|
59
|
+
.required(),
|
|
26
60
|
}).required();
|
|
27
61
|
|
|
28
62
|
export const configSchema = Joi.object({
|
package/src/validate.test.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateReportingEvent } from "./validate.js";
|
|
2
|
+
import { AGREEMENT_CREATED, AGREEMENT_STATUS_CHANGED } from "./constants.js";
|
|
2
3
|
|
|
3
4
|
const validBase = {
|
|
4
5
|
correlationId: "abc-123",
|
|
@@ -9,9 +10,11 @@ const validBase = {
|
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
const eventDataPayload = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
eventType: AGREEMENT_STATUS_CHANGED,
|
|
14
|
+
agreementId: "WMP123456789",
|
|
15
|
+
agreementStatus: "ACCEPTED",
|
|
16
|
+
statusDate: "2025-12-01T12:51:41.381Z",
|
|
17
|
+
sbi: "abc123",
|
|
15
18
|
};
|
|
16
19
|
|
|
17
20
|
describe("validateReportingEvent", () => {
|
|
@@ -25,24 +28,27 @@ describe("validateReportingEvent", () => {
|
|
|
25
28
|
expect(result.value).toBeDefined();
|
|
26
29
|
});
|
|
27
30
|
|
|
28
|
-
test("accepts optional
|
|
31
|
+
test("accepts optional agreementStatus and details fields", () => {
|
|
29
32
|
const result = validateReportingEvent({
|
|
30
33
|
...validBase,
|
|
31
34
|
eventData: {
|
|
32
|
-
|
|
35
|
+
eventType: AGREEMENT_STATUS_CHANGED,
|
|
36
|
+
agreementId: "WMP123456789",
|
|
37
|
+
agreementStatus: "created",
|
|
38
|
+
statusDate: "2025-12-01T12:51:41.381Z",
|
|
33
39
|
details: { agreementId: "agreement_456" },
|
|
34
40
|
},
|
|
35
41
|
});
|
|
36
42
|
expect(result.valid).toBe(true);
|
|
37
43
|
});
|
|
38
44
|
|
|
39
|
-
test("normalises the validated value (
|
|
45
|
+
test("normalises the validated value (converts datetime to Date object)", () => {
|
|
40
46
|
const result = validateReportingEvent({
|
|
41
47
|
...validBase,
|
|
42
48
|
eventData: eventDataPayload,
|
|
43
49
|
});
|
|
44
50
|
expect(result.valid).toBe(true);
|
|
45
|
-
expect(result.value.
|
|
51
|
+
expect(result.value.datetime).toBeInstanceOf(Date);
|
|
46
52
|
});
|
|
47
53
|
});
|
|
48
54
|
|
|
@@ -99,10 +105,212 @@ describe("validateReportingEvent", () => {
|
|
|
99
105
|
expect(result.errors.some((e) => e.includes("version"))).toBe(true);
|
|
100
106
|
});
|
|
101
107
|
|
|
108
|
+
test("returns valid:false when eventType is invalid", () => {
|
|
109
|
+
const result = validateReportingEvent({
|
|
110
|
+
...validBase,
|
|
111
|
+
eventData: {
|
|
112
|
+
...eventDataPayload,
|
|
113
|
+
eventType: "INVALID_EVENT_TYPE",
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
expect(result.valid).toBe(false);
|
|
117
|
+
expect(result.errors).toContain(
|
|
118
|
+
'"eventData.eventType" must be one of [AGREEMENT_CREATED, AGREEMENT_STATUS_CHANGED]',
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
102
122
|
test("returns multiple errors with abortEarly:false", () => {
|
|
103
123
|
const result = validateReportingEvent({ eventData: eventDataPayload });
|
|
104
124
|
expect(result.valid).toBe(false);
|
|
105
125
|
expect(result.errors.length).toBeGreaterThan(1);
|
|
106
126
|
});
|
|
107
127
|
});
|
|
128
|
+
|
|
129
|
+
describe("Agreement created events", () => {
|
|
130
|
+
test("accepts a valid Agreement created event", () => {
|
|
131
|
+
const result = validateReportingEvent({
|
|
132
|
+
...validBase,
|
|
133
|
+
eventData: {
|
|
134
|
+
eventType: AGREEMENT_CREATED,
|
|
135
|
+
agreementId: "WMP123456789",
|
|
136
|
+
agreementType: "WOODLAND",
|
|
137
|
+
agreementStatus: "ACCEPTED",
|
|
138
|
+
agreementStartDate: "2026-09-01",
|
|
139
|
+
agreementEndDate: "2029-08-31",
|
|
140
|
+
agreementValue: 1575.0,
|
|
141
|
+
sbi: "200000001",
|
|
142
|
+
options: [],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
expect(result.valid).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("accepts Agreement created event with options", () => {
|
|
149
|
+
const result = validateReportingEvent({
|
|
150
|
+
...validBase,
|
|
151
|
+
eventData: {
|
|
152
|
+
eventType: AGREEMENT_CREATED,
|
|
153
|
+
agreementId: "WMP123456789",
|
|
154
|
+
agreementType: "WOODLAND",
|
|
155
|
+
agreementStatus: "ACCEPTED",
|
|
156
|
+
sbi: "200000001",
|
|
157
|
+
options: [
|
|
158
|
+
{
|
|
159
|
+
parcelReference: "SD8545-9935",
|
|
160
|
+
parcelSizeUnderAgreement: 15.75,
|
|
161
|
+
optionCode: "WMP1",
|
|
162
|
+
optionYear: 1,
|
|
163
|
+
optionStartDate: "2026-09-01",
|
|
164
|
+
optionEndDate: "2029-08-31",
|
|
165
|
+
optionQuantity: 15.75,
|
|
166
|
+
optionValue: 1575.0,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
expect(result.valid).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("accepts Agreement created event without optional fields", () => {
|
|
175
|
+
const result = validateReportingEvent({
|
|
176
|
+
...validBase,
|
|
177
|
+
eventData: {
|
|
178
|
+
eventType: AGREEMENT_CREATED,
|
|
179
|
+
agreementId: "WMP123456789",
|
|
180
|
+
agreementType: "WOODLAND",
|
|
181
|
+
agreementStatus: "ACCEPTED",
|
|
182
|
+
sbi: "200000001",
|
|
183
|
+
options: [],
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
expect(result.valid).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("accepts Agreement created event with minimal options", () => {
|
|
190
|
+
const result = validateReportingEvent({
|
|
191
|
+
...validBase,
|
|
192
|
+
eventData: {
|
|
193
|
+
eventType: AGREEMENT_CREATED,
|
|
194
|
+
agreementId: "WMP123456789",
|
|
195
|
+
agreementType: "WOODLAND",
|
|
196
|
+
agreementStatus: "ACCEPTED",
|
|
197
|
+
sbi: "200000001",
|
|
198
|
+
options: [
|
|
199
|
+
{
|
|
200
|
+
parcelReference: "SD8545-9935",
|
|
201
|
+
optionCode: "WMP1",
|
|
202
|
+
optionStartDate: "2026-09-01",
|
|
203
|
+
optionEndDate: "2029-08-31",
|
|
204
|
+
optionQuantity: 15.75,
|
|
205
|
+
optionValue: 1575.0,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
expect(result.valid).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("rejects Agreement created event if required fields are missing", () => {
|
|
214
|
+
const result = validateReportingEvent({
|
|
215
|
+
...validBase,
|
|
216
|
+
eventData: {
|
|
217
|
+
eventType: AGREEMENT_CREATED,
|
|
218
|
+
agreementId: "WMP123456789",
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
expect(result.valid).toBe(false);
|
|
222
|
+
expect(result.errors).toContain('"eventData.agreementType" is required');
|
|
223
|
+
expect(result.errors).toContain(
|
|
224
|
+
'"eventData.agreementStatus" is required',
|
|
225
|
+
);
|
|
226
|
+
expect(result.errors).toContain('"eventData.sbi" is required');
|
|
227
|
+
expect(result.errors).toContain('"eventData.options" is required');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("rejects Agreement created event if options contain invalid data", () => {
|
|
231
|
+
const result = validateReportingEvent({
|
|
232
|
+
...validBase,
|
|
233
|
+
eventData: {
|
|
234
|
+
eventType: AGREEMENT_CREATED,
|
|
235
|
+
agreementId: "WMP123456789",
|
|
236
|
+
agreementType: "WOODLAND",
|
|
237
|
+
agreementStatus: "ACCEPTED",
|
|
238
|
+
sbi: "200000001",
|
|
239
|
+
options: [
|
|
240
|
+
{
|
|
241
|
+
agreementId: "WMP123456789",
|
|
242
|
+
// missing mandatory option fields
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
expect(result.valid).toBe(false);
|
|
248
|
+
expect(result.errors).toContain(
|
|
249
|
+
'"eventData.options[0].parcelReference" is required',
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("Agreement status changed events", () => {
|
|
255
|
+
test("accepts a valid Agreement status changed event with all fields", () => {
|
|
256
|
+
const result = validateReportingEvent({
|
|
257
|
+
...validBase,
|
|
258
|
+
eventData: {
|
|
259
|
+
eventType: AGREEMENT_STATUS_CHANGED,
|
|
260
|
+
agreementId: "WMP123456789",
|
|
261
|
+
agreementStatus: "ACCEPTED",
|
|
262
|
+
agreementStartDate: "2026-09-01",
|
|
263
|
+
agreementEndDate: "2029-08-31",
|
|
264
|
+
statusDate: "2026-09-02T10:00:00Z",
|
|
265
|
+
agreementValue: 1575.0,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
expect(result.valid).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("accepts a valid Agreement status changed event with all fields including userId", () => {
|
|
272
|
+
const result = validateReportingEvent({
|
|
273
|
+
...validBase,
|
|
274
|
+
eventData: {
|
|
275
|
+
eventType: AGREEMENT_STATUS_CHANGED,
|
|
276
|
+
agreementId: "WMP123456789",
|
|
277
|
+
agreementStatus: "ACCEPTED",
|
|
278
|
+
agreementStartDate: "2026-09-01",
|
|
279
|
+
agreementEndDate: "2029-08-31",
|
|
280
|
+
statusDate: "2026-09-02T10:00:00Z",
|
|
281
|
+
agreementValue: 1575.0,
|
|
282
|
+
userId: "user-123",
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
expect(result.valid).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("accepts a valid Agreement status changed event with only mandatory fields", () => {
|
|
289
|
+
const result = validateReportingEvent({
|
|
290
|
+
...validBase,
|
|
291
|
+
eventData: {
|
|
292
|
+
eventType: AGREEMENT_STATUS_CHANGED,
|
|
293
|
+
agreementId: "WMP123456789",
|
|
294
|
+
agreementStatus: "ACCEPTED",
|
|
295
|
+
statusDate: "2026-09-02T10:00:00Z",
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
expect(result.valid).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("rejects Agreement status changed event if required fields are missing", () => {
|
|
302
|
+
const result = validateReportingEvent({
|
|
303
|
+
...validBase,
|
|
304
|
+
eventData: {
|
|
305
|
+
eventType: AGREEMENT_STATUS_CHANGED,
|
|
306
|
+
agreementId: "WMP123456789",
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
expect(result.valid).toBe(false);
|
|
310
|
+
expect(result.errors).toContain(
|
|
311
|
+
'"eventData.agreementStatus" is required',
|
|
312
|
+
);
|
|
313
|
+
expect(result.errors).toContain('"eventData.statusDate" is required');
|
|
314
|
+
});
|
|
315
|
+
});
|
|
108
316
|
});
|