@defra/grants-reporting-publisher 0.0.2 → 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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @defra/grants-reporting-publisher
2
2
 
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 94ffa84: Add the schema definition for input events
8
+
9
+ ## 0.0.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 6be70d1: Allow consumers to use validation without requiring SNS client
14
+
3
15
  ## 0.0.2
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/grants-reporting-publisher",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "NPM module for publishing grants reporting events",
5
5
  "engines": {
6
6
  "node": ">=24",
@@ -27,9 +27,13 @@
27
27
  "version:create": "changeset"
28
28
  },
29
29
  "author": "Defra Grants Platform Experience Team",
30
- "main": "./dist/index.js",
30
+ "main": "./src/index.js",
31
31
  "exports": {
32
- ".": "./dist/index.js"
32
+ ".": "./src/index.js",
33
+ "./publish": "./src/publish.js",
34
+ "./validate": "./src/validate.js",
35
+ "./schema": "./src/schema.js",
36
+ "./constants": "./src/constants.js"
33
37
  },
34
38
  "license": "OGL-UK-3.0",
35
39
  "bugs": {
@@ -61,6 +65,11 @@
61
65
  "peerDependencies": {
62
66
  "@aws-sdk/client-sns": ">=3"
63
67
  },
68
+ "peerDependenciesMeta": {
69
+ "@aws-sdk/client-sns": {
70
+ "optional": true
71
+ }
72
+ },
64
73
  "release-it": {
65
74
  "npm": {
66
75
  "publish": false
@@ -0,0 +1,2 @@
1
+ export const AGREEMENT_CREATED = "AGREEMENT_CREATED";
2
+ export const AGREEMENT_STATUS_CHANGED = "AGREEMENT_STATUS_CHANGED";
package/src/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { validateReportingEvent } from "./validate.js";
2
- export { publishReportingEvent } from "./publish.js";
2
+ export * from "./constants.js";
@@ -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
- status: "created",
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
- accounts: Joi.object({
21
- sbi: accountId,
22
- }).default({}),
23
- status: Joi.string().max(MAX_120).allow(""),
24
- details: Joi.object().default({}),
25
- }).required(),
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({
@@ -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
- accounts: {
13
- sbi: "abc123",
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 status and details fields", () => {
31
+ test("accepts optional agreementStatus and details fields", () => {
29
32
  const result = validateReportingEvent({
30
33
  ...validBase,
31
34
  eventData: {
32
- status: "created",
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 (applies joi defaults)", () => {
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.eventData.details).toEqual({});
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
  });