Skip to content

Commit 6556a7b

Browse files
committed
feat(agreements): make RI stateful
Signed-off-by: mttrbrts <[email protected]>
1 parent 057b651 commit 6556a7b

File tree

4 files changed

+313
-54
lines changed

4 files changed

+313
-54
lines changed

server/handlers/agreements.test.ts

Lines changed: 69 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ async function createLateDeliveryTemplate(): Promise<ApTemplate> {
4949

5050
const buffer = zip.toBuffer();
5151
const template = await ApTemplate.fromArchive(buffer);
52-
await template.getModelManager().updateExternalModels();
5352
return template;
5453
}
5554

@@ -68,7 +67,9 @@ describe('Agreements Router - POST /:id/trigger', () => {
6867
// Mock database
6968
mockDb = {
7069
select: jest.fn().mockReturnThis(),
70+
update: jest.fn().mockReturnThis(),
7171
from: jest.fn().mockReturnThis(),
72+
set: jest.fn().mockReturnThis(),
7273
where: jest.fn().mockReturnThis(),
7374
limit: jest.fn().mockReturnThis(),
7475
};
@@ -153,7 +154,7 @@ describe('Agreements Router - POST /:id/trigger', () => {
153154
mockDb.limit.mockReturnValue(mockDb);
154155

155156
// Mock agreement query result
156-
mockDb.limit.mockImplementationOnce(() => Promise.resolve([mockAgreementData]));
157+
mockDb.limit.mockImplementationOnce(() => Promise.resolve([{ ...mockAgreementData }]));
157158

158159
// Mock template query result
159160
mockDb.limit.mockImplementationOnce(() => Promise.resolve([mockTemplateData]));
@@ -163,6 +164,10 @@ describe('Agreements Router - POST /:id/trigger', () => {
163164
});
164165

165166
it('should successfully trigger agreement with valid request', async () => {
167+
168+
mockDb.update.mockReturnValue(mockDb);
169+
mockDb.set.mockReturnValue(mockDb);
170+
166171
const triggerRequest = {
167172
$class: 'io.clause.latedeliveryandpenalty@0.1.0.LateDeliveryAndPenaltyRequest',
168173
forceMajeure: false,
@@ -182,9 +187,14 @@ describe('Agreements Router - POST /:id/trigger', () => {
182187
expect(response.body.result).toHaveProperty('buyerMayTerminate');
183188
expect(typeof response.body.result.penalty).toBe('number');
184189
expect(typeof response.body.result.buyerMayTerminate).toBe('boolean');
190+
expect(response.body.state.count).toBe(1);
185191
});
186192

187193
it('should handle trigger with goods delivered on time', async () => {
194+
195+
mockDb.update.mockReturnValue(mockDb);
196+
mockDb.set.mockReturnValue(mockDb);
197+
188198
const triggerRequest = {
189199
$class: 'io.clause.latedeliveryandpenalty@0.1.0.LateDeliveryAndPenaltyRequest',
190200
forceMajeure: false,
@@ -202,9 +212,14 @@ describe('Agreements Router - POST /:id/trigger', () => {
202212
// TODO fix template to only calculate penalty when delivery is late
203213
expect(response.body.result.penalty).toBe(17500); // No penalty for on-time delivery
204214
expect(response.body.result.buyerMayTerminate).toBe(true);
215+
expect(response.body.state.count).toBe(1);
205216
});
206217

207218
it('should handle trigger with goods not yet delivered', async () => {
219+
220+
mockDb.update.mockReturnValue(mockDb);
221+
mockDb.set.mockReturnValue(mockDb);
222+
208223
const triggerRequest = {
209224
$class: 'io.clause.latedeliveryandpenalty@0.1.0.LateDeliveryAndPenaltyRequest',
210225
forceMajeure: false,
@@ -221,6 +236,58 @@ describe('Agreements Router - POST /:id/trigger', () => {
221236
expect(response.body.result).toHaveProperty('$class', 'io.clause.latedeliveryandpenalty@0.1.0.LateDeliveryAndPenaltyResponse');
222237
expect(response.body.result).toHaveProperty('penalty');
223238
expect(response.body.result).toHaveProperty('buyerMayTerminate');
239+
expect(response.body.state.count).toBe(1);
240+
});
241+
242+
it('should successfully trigger agreement with valid request multiple times remembering state', async () => {
243+
244+
mockDb.update.mockReturnValue(mockDb);
245+
mockDb.set.mockReturnValue(mockDb);
246+
247+
const triggerRequest = {
248+
$class: 'io.clause.latedeliveryandpenalty@0.1.0.LateDeliveryAndPenaltyRequest',
249+
forceMajeure: false,
250+
agreedDelivery: '2024-01-01T00:00:00Z',
251+
deliveredAt: '2024-01-15T00:00:00Z', // 14 days late
252+
goodsValue: 1000.00
253+
};
254+
255+
const response1 = await request(app)
256+
.post('/agreements/1/trigger')
257+
.send(triggerRequest)
258+
.expect(200);
259+
260+
expect(response1.body.state.count).toBe(1);
261+
262+
263+
// Mock successful database queries
264+
mockDb.select.mockReturnValue(mockDb);
265+
mockDb.update.mockReturnValue(mockDb);
266+
mockDb.from.mockReturnValue(mockDb);
267+
mockDb.set.mockReturnValue(mockDb);
268+
mockDb.where.mockReturnValue(mockDb);
269+
mockDb.limit.mockReturnValue(mockDb);
270+
271+
// Mock the state response with the state from the first request
272+
mockDb.limit.mockImplementationOnce(() => Promise.resolve([{
273+
...mockAgreementData, state: response1.body.state
274+
}]));
275+
mockDb.limit.mockImplementationOnce(() => Promise.resolve([mockTemplateData]));
276+
277+
const response2 = await request(app)
278+
.post('/agreements/1/trigger')
279+
.send(triggerRequest)
280+
.expect(200);
281+
282+
// Verify response structure
283+
expect(response2.body.result).toHaveProperty('$class', 'io.clause.latedeliveryandpenalty@0.1.0.LateDeliveryAndPenaltyResponse');
284+
expect(response2.body.result).toHaveProperty('penalty');
285+
expect(response2.body.result).toHaveProperty('buyerMayTerminate');
286+
expect(typeof response2.body.result.penalty).toBe('number');
287+
expect(typeof response2.body.result.buyerMayTerminate).toBe('boolean');
288+
289+
expect(response2.body.state.count).toBe(2);
290+
224291
});
225292
});
226293

@@ -444,54 +511,5 @@ describe('Agreements Router - POST /:id/trigger', () => {
444511
expect(mockDb.from).toHaveBeenCalledWith(Template);
445512
expect(mockDb.limit).toHaveBeenCalledWith(1);
446513
});
447-
448-
it('should parse agreement ID as integer', async () => {
449-
const mockAgreementData = {
450-
id: 123,
451-
template: 'test://template/1',
452-
data: {
453-
$class: '[email protected]',
454-
clauseId: 'latedelivery-1',
455-
forceMajeure: false,
456-
penaltyDuration: { $class: '[email protected]', amount: 9, unit: 'DAY' },
457-
penaltyPercentage: 7.0,
458-
capPercentage: 2.0,
459-
termination: { $class: '[email protected]', amount: 2, unit: 'WEEK' },
460-
fractionalPart: 'DAY'
461-
}
462-
};
463-
const mockTemplateData = {
464-
id: 1,
465-
uri: 'test://template/1',
466-
metadata: {
467-
runtime: 'typescript',
468-
template: 'clause',
469-
cicero: '0.25.0'
470-
}
471-
};
472-
473-
mockDb.limit
474-
.mockResolvedValueOnce([mockAgreementData])
475-
.mockResolvedValueOnce([mockTemplateData]);
476-
477-
const realApTemplate = await createLateDeliveryTemplate();
478-
mockedTemplateBuilder.templateFromDatabase.mockResolvedValue(realApTemplate);
479-
480-
const triggerRequest = {
481-
$class: 'io.clause.latedeliveryandpenalty@0.1.0.LateDeliveryAndPenaltyRequest',
482-
forceMajeure: false,
483-
agreedDelivery: '2024-01-01T00:00:00Z',
484-
deliveredAt: '2024-01-10T00:00:00Z',
485-
goodsValue: 1000.00
486-
};
487-
488-
await request(app)
489-
.post('/agreements/123/trigger')
490-
.send(triggerRequest)
491-
.expect(200);
492-
493-
// The ID should be parsed as integer in the where clause
494-
expect(mockDb.where).toHaveBeenCalled();
495-
});
496514
});
497515
});

server/handlers/agreements.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ crudRouter.post('/:id/trigger', async function (req, res) {
7777
if (!requestSchema) {
7878
throw new Error(`Invalid request type: ${req.body.$class}`);
7979
}
80-
// TODO validate request body against apTemplate
8180
const { success, error } = await concertoValidation(req.body.$class, req.body, apTemplate.getModelManager());
8281

8382
if (!success){
@@ -95,9 +94,14 @@ crudRouter.post('/:id/trigger', async function (req, res) {
9594
agreement.state = state.state;
9695
}
9796
const triggerResult = await templateArchiveProcessor.trigger(agreement.data, req.body, agreement.state);
97+
agreement.state = triggerResult.state;
9898
console.log(JSON.stringify(triggerResult));
9999

100-
// TODO persist updated state.
100+
// Persist updated state.
101+
await res.locals.db
102+
.update(Agreement)
103+
.set(agreement)
104+
.where(eq(Agreement.id, Number.parseInt(agreement.id)));
101105

102106
res.json(triggerResult);
103107
}

server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"@accordproject/cicero-core": "0.25.1-20250328175253",
2828
"@accordproject/concerto-core": "^3.21.0",
2929
"@accordproject/concerto-util": "^3.20.4",
30-
"@accordproject/template-engine": "^2.7.0",
30+
"@accordproject/template-engine": "^2.7.1",
3131
"@modelcontextprotocol/sdk": "^1.15.1",
3232
"adm-zip": "^0.5.16",
3333
"crypto": "^1.0.1",

0 commit comments

Comments
 (0)