diff --git a/backend/models/__tests__/pingContract.test.js b/backend/models/__tests__/pingContract.test.js index 4280fce..0f7c1a6 100644 --- a/backend/models/__tests__/pingContract.test.js +++ b/backend/models/__tests__/pingContract.test.js @@ -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', () => { @@ -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', () => { @@ -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'); + }); }); diff --git a/backend/models/pingContract.js b/backend/models/pingContract.js index 880b8a7..3ffb9e3 100644 --- a/backend/models/pingContract.js +++ b/backend/models/pingContract.js @@ -1,3 +1,5 @@ +const { randomUUID } = require('crypto'); + const PING_CONTRACT_TABLE_NAME = "PingContracts"; /** @@ -21,7 +23,7 @@ const PING_CONTRACT_TABLE_NAME = "PingContracts"; * @param {string} [params.status] */ function createPingContractItem({ - id, + id = randomUUID(), contractType, producerId, consumerId = null, @@ -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)'); } @@ -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 = [ @@ -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)'); } @@ -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 }; diff --git a/backend/package.json b/backend/package.json index e4b59f0..4a715c6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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" } }