Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 64 additions & 20 deletions backend/models/__tests__/pingContract.test.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
const {
createPingContractItem,
createConsumerPlan,
createProducerPlan,
createOpenPingPost,
addNegotiation,
addSignature,
addPerformanceUpdate
addPerformanceUpdate,
updateLbtasRating,
renewContract
} = require('../pingContract');

describe('pingContract model', () => {
test('creates item with required fields', () => {
const item = createPingContractItem({
test('auto assigns id and template types', () => {
const consumer = createConsumerPlan({
producerId: 'p1',
pingRate: 'daily',
shareType: 'fixed'
});
const producer = createProducerPlan({
id: 'PING1',
contractType: 'producer',
producerId: 'prod1',
producerId: 'p2',
pingRate: 'weekly',
shareType: 'fixed',
shareValue: 50
shareType: 'variable'
});
const open = createOpenPingPost({
producerId: 'p3',
pingRate: 'monthly',
shareType: 'fixed'
});

expect(item.id).toBe('PING1');
expect(item.contractType).toBe('producer');
expect(item.pingRate).toBe('weekly');
expect(item.shareType).toBe('fixed');
expect(item.shareValue).toBe(50);
expect(item.attachments).toEqual([]);
expect(consumer.id).toBeDefined();
expect(consumer.contractType).toBe('consumer');
expect(producer.id).toBe('PING1');
expect(producer.contractType).toBe('producer');
expect(open.contractType).toBe('open');
});

test('throws when attachments exceed limit', () => {
Expand All @@ -37,22 +49,40 @@ describe('pingContract model', () => {
).toThrow('Attachments limit exceeded');
});

test('negotiation, signature and performance updates append', () => {
const contract = createPingContractItem({
id: 'PING3',
contractType: 'open',
test('requires attachments to be an array', () => {
expect(() =>
createPingContractItem({
id: 'PING5',
contractType: 'consumer',
producerId: 'prod1',
pingRate: 'daily',
shareType: 'fixed',
attachments: 'not-array'
})
).toThrow('Attachments must be an array');
});

test('negotiation, signature, performance, rating and renewal work', () => {
let contract = createPingContractItem({
producerId: 'prod1',
contractType: 'open',
pingRate: 'monthly',
shareType: 'variable'
});

addNegotiation(contract, { partyId: 'user1', message: 'offer A' });
addSignature(contract, { partyId: 'user1', signature: 'sig' });
addPerformanceUpdate(contract, { update: 'on track', attachments: ['a'] });
contract = addNegotiation(contract, { partyId: 'user1', message: 'offer A' });
contract = addSignature(contract, { partyId: 'user1', signature: 'sig' });
contract = addPerformanceUpdate(contract, { update: 'on track', attachments: ['a'] });
contract = updateLbtasRating(contract, 5);
contract = renewContract(contract, { renewalDate: '2025-01-01', calendarEventId: 'cal1' });

expect(contract.negotiationHistory.length).toBe(1);
expect(contract.signatures.length).toBe(1);
expect(contract.performanceUpdates.length).toBe(1);
expect(contract.lbtasRating).toBe(5);
expect(contract.autoRenew).toBe(true);
expect(contract.renewalDate).toBe('2025-01-01');
expect(contract.calendarEventId).toBe('cal1');
});

test('performance update attachments limit enforced', () => {
Expand All @@ -68,4 +98,18 @@ describe('pingContract model', () => {
addPerformanceUpdate(contract, { update: 'fail', attachments: [1,2,3,4,5,6] })
).toThrow('Attachments limit exceeded');
});

test('performance update attachments must be array', () => {
const contract = createPingContractItem({
id: 'PING6',
contractType: 'open',
producerId: 'prod1',
pingRate: 'monthly',
shareType: 'variable'
});

expect(() =>
addPerformanceUpdate(contract, { update: 'bad', attachments: 'oops' })
).toThrow('Attachments must be an array');
});
});
45 changes: 43 additions & 2 deletions backend/models/pingContract.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { randomUUID } = require('crypto');

const PING_CONTRACT_TABLE_NAME = "PingContracts";

/**
Expand All @@ -21,7 +23,7 @@ const PING_CONTRACT_TABLE_NAME = "PingContracts";
* @param {string} [params.status]
*/
function createPingContractItem({
id,
id = randomUUID(),
contractType,
producerId,
consumerId = null,
Expand All @@ -38,6 +40,9 @@ function createPingContractItem({
calendarEventId = null,
status = 'open'
}) {
if (!Array.isArray(attachments)) {
throw new Error('Attachments must be an array');
}
if (attachments.length > 5) {
throw new Error('Attachments limit exceeded (max 5)');
}
Expand All @@ -62,6 +67,19 @@ function createPingContractItem({
};
}

/** Convenience templates */
function createConsumerPlan(params) {
return createPingContractItem({ ...params, contractType: 'consumer' });
}

function createProducerPlan(params) {
return createPingContractItem({ ...params, contractType: 'producer' });
}

function createOpenPingPost(params) {
return createPingContractItem({ ...params, contractType: 'open' });
}

/** Add a negotiation entry */
function addNegotiation(contract, { partyId, message, timestamp = new Date().toISOString() }) {
const negotiationHistory = [
Expand All @@ -82,6 +100,9 @@ function addSignature(contract, { partyId, signature, signedAt = new Date().toIS

/** Add a performance update with optional attachments (max 5) */
function addPerformanceUpdate(contract, { update, attachments = [], timestamp = new Date().toISOString() }) {
if (!Array.isArray(attachments)) {
throw new Error('Attachments must be an array');
}
if (attachments.length > 5) {
throw new Error('Attachments limit exceeded (max 5)');
}
Expand All @@ -90,10 +111,30 @@ function addPerformanceUpdate(contract, { update, attachments = [], timestamp =
return { ...contract, performanceUpdates };
}

/** Update LBTAS rating */
function updateLbtasRating(contract, rating) {
return { ...contract, lbtasRating: rating };
}

/** Renew contract and link to calendar event */
function renewContract(contract, { renewalDate, calendarEventId = null }) {
return {
...contract,
autoRenew: true,
renewalDate,
calendarEventId
};
}

module.exports = {
PING_CONTRACT_TABLE_NAME,
createPingContractItem,
createConsumerPlan,
createProducerPlan,
createOpenPingPost,
addNegotiation,
addSignature,
addPerformanceUpdate
addPerformanceUpdate,
updateLbtasRating,
renewContract
};
6 changes: 6 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
"twilio": "^4.21.0"
},
"devDependencies": {
"aws-sdk": "^2.1534.0",
"jest": "^29.7.0",
"supertest": "^7.1.1"
},
"jest": {
"testEnvironment": "node"
"aws-sdk": "^2.1534.0"
}
}