UNPKG

@defra-fish/sales-api-service

Version:
536 lines (494 loc) 21.5 kB
import { processQueue, getTransactionJournalRefNumber } from '../process-transaction-queue.js' import { persist, ConcessionProof, Contact, FulfilmentRequest, Permission, RecurringPayment, Transaction, TransactionJournal } from '@defra-fish/dynamics-lib' import { mockFinalisedTransactionRecord, MOCK_1DAY_SENIOR_PERMIT_ENTITY, MOCK_12MONTH_SENIOR_PERMIT, MOCK_12MONTH_DISABLED_PERMIT, MOCK_CONCESSION, MOCK_TRANSACTION_CURRENCY, mockContactPayload, MOCK_EXISTING_CONTACT_ENTITY } from '../../../__mocks__/test-data.js' import { TRANSACTION_STAGING_TABLE, TRANSACTION_STAGING_HISTORY_TABLE } from '../../../config.js' import { POCL_DATA_SOURCE, DDE_DATA_SOURCE } from '@defra-fish/business-rules-lib' import moment from 'moment' import { processRecurringPayment, generateRecurringPaymentRecord, findNewestExistingRecurringPaymentInCrm } from '../../recurring-payments.service.js' import { AWS } from '@defra-fish/connectors-lib' const { docClient } = AWS.mock.results[0].value jest.mock('../../reference-data.service.js', () => ({ ...jest.requireActual('../../reference-data.service.js'), getReferenceDataForEntity: async entityType => { if (entityType === MOCK_TRANSACTION_CURRENCY.constructor) { return [MOCK_TRANSACTION_CURRENCY] } return [] }, getReferenceDataForEntityAndId: jest.fn(async (entityType, id) => { let item = null if (entityType === MOCK_12MONTH_SENIOR_PERMIT.constructor) { item = [MOCK_12MONTH_SENIOR_PERMIT, MOCK_12MONTH_DISABLED_PERMIT, MOCK_1DAY_SENIOR_PERMIT_ENTITY].find(p => p.id === id) } else if (entityType === MOCK_CONCESSION.constructor) { item = MOCK_CONCESSION } return item }) })) jest.mock('@defra-fish/dynamics-lib', () => ({ ...jest.requireActual('@defra-fish/dynamics-lib'), persist: jest.fn() })) jest.mock('../../contacts.service.js', () => ({ ...jest.requireActual('../../contacts.service.js'), resolveContactPayload: async () => MOCK_EXISTING_CONTACT_ENTITY })) jest.mock('@defra-fish/business-rules-lib', () => ({ POCL_DATA_SOURCE: 'POCL_DATA_SOURCE', DDE_DATA_SOURCE: 'DDE_DATA_SOURCE', POCL_TRANSACTION_SOURCES: ['POCL_DATA_SOURCE', 'DDE_DATA_SOURCE'], START_AFTER_PAYMENT_MINUTES: 30 })) jest.mock('../../recurring-payments.service.js', () => ({ findNewestExistingRecurringPaymentInCrm: jest.fn(), generateRecurringPaymentRecord: jest.fn(), processRecurringPayment: jest.fn() })) jest.mock('@defra-fish/connectors-lib', () => { const mockAWS = { docClient: { get: jest.fn(), delete: jest.fn(), put: jest.fn() } } return { AWS: jest.fn(() => mockAWS) } }) describe('transaction service', () => { beforeAll(() => { TRANSACTION_STAGING_TABLE.TableName = 'TestTable' processRecurringPayment.mockResolvedValue({}) }) beforeEach(jest.clearAllMocks) describe('processQueue', () => { describe('processes messages related to different licence types', () => { it.each([ [ 'short term licences', () => { const mockRecord = mockFinalisedTransactionRecord() mockRecord.permissions[0].permitId = MOCK_1DAY_SENIOR_PERMIT_ENTITY.id return mockRecord }, [ expect.any(Transaction), expect.any(TransactionJournal), expect.any(TransactionJournal), expect.any(Contact), expect.any(Permission), expect.any(ConcessionProof) ] ], [ 'long term licences', () => { const mockRecord = mockFinalisedTransactionRecord() mockRecord.permissions[0].permitId = MOCK_12MONTH_SENIOR_PERMIT.id return mockRecord }, [ expect.any(Transaction), expect.any(TransactionJournal), expect.any(TransactionJournal), expect.any(Contact), expect.any(Permission), expect.any(ConcessionProof) ] ], [ 'long term licences (no concession)', () => { const mockRecord = mockFinalisedTransactionRecord() mockRecord.permissions[0].permitId = MOCK_12MONTH_SENIOR_PERMIT.id delete mockRecord.permissions[0].concessions return mockRecord }, [ expect.any(Transaction), expect.any(TransactionJournal), expect.any(TransactionJournal), expect.any(Contact), expect.any(Permission) ] ], [ 'licences with a recurring payment', () => { processRecurringPayment.mockResolvedValueOnce({ recurringPayment: new RecurringPayment() }) const mockRecord = mockFinalisedTransactionRecord() mockRecord.payment.recurring = { name: 'Test name', nextDueDate: new Date('2020/01/11'), endDate: new Date('2022/01/16'), agreementId: '123446jjng', publicId: 'sdf-123', status: 1, activePermission: mockRecord.permissions[0], contact: Object.assign(mockContactPayload(), { firstName: 'Esther' }) } mockRecord.permissions[0].permitId = MOCK_12MONTH_SENIOR_PERMIT.id return mockRecord }, [ expect.any(Transaction), expect.any(TransactionJournal), expect.any(TransactionJournal), expect.any(Contact), expect.any(Permission), expect.any(RecurringPayment), expect.any(ConcessionProof) ] ], [ 'licences with a previous recurring payment linked to new recurring payment', () => { processRecurringPayment.mockResolvedValueOnce({ recurringPayment: new RecurringPayment() }) findNewestExistingRecurringPaymentInCrm.mockResolvedValueOnce(new RecurringPayment()) const mockRecord = mockFinalisedTransactionRecord() mockRecord.payment.recurring = { name: 'Test name', nextDueDate: new Date('2020/01/11'), endDate: new Date('2022/01/16'), agreementId: '123446jjng', publicId: 'sdf-123', status: 1, activePermission: mockRecord.permissions[0], contact: Object.assign(mockContactPayload(), { firstName: 'Esther' }) } mockRecord.permissions[0].permitId = MOCK_12MONTH_SENIOR_PERMIT.id return mockRecord }, [ expect.any(Transaction), expect.any(TransactionJournal), expect.any(TransactionJournal), expect.any(Contact), expect.any(Permission), expect.any(RecurringPayment), expect.any(RecurringPayment), expect.any(ConcessionProof) ] ] ])('handles %s', async (description, initialiseMockTransactionRecord, entityExpectations) => { const mockRecord = initialiseMockTransactionRecord() docClient.get.mockResolvedValueOnce({ Item: mockRecord }) const result = await processQueue({ id: mockRecord.id }) expect(result).toBeUndefined() expect(persist).toBeCalledWith(entityExpectations, undefined) expect(docClient.get).toHaveBeenCalledWith( expect.objectContaining({ TableName: TRANSACTION_STAGING_TABLE.TableName, Key: { id: mockRecord.id }, ConsistentRead: true }) ) expect(docClient.delete).toHaveBeenCalledWith( expect.objectContaining({ TableName: TRANSACTION_STAGING_TABLE.TableName, Key: { id: mockRecord.id } }) ) const expectedRecord = Object.assign(mockRecord, { id: expect.stringMatching(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i), expires: expect.any(Number) }) expect(docClient.put).toHaveBeenCalledWith( expect.objectContaining({ TableName: TRANSACTION_STAGING_HISTORY_TABLE.TableName, Item: expectedRecord, ConditionExpression: 'attribute_not_exists(id)' }) ) }) }) describe('when the fulfilment change date has not yet passed', () => { beforeEach(() => { process.env.FULFILMENT_SWITCHOVER_DATE = moment().add(1, 'day').toISOString() }) afterEach(() => { delete process.env.FULFILMENT_SWITCHOVER_DATE }) it('includes a FulfilmentRequest when the permit and contact are for postal fulfilment', async () => { const mockRecord = mockFinalisedTransactionRecord() mockRecord.permissions[0].permitId = MOCK_12MONTH_SENIOR_PERMIT.id docClient.get.mockResolvedValueOnce({ Item: mockRecord }) await processQueue({ id: mockRecord.id }) expect(persist).toBeCalledWith( [ expect.any(Transaction), expect.any(TransactionJournal), expect.any(TransactionJournal), expect.any(Contact), expect.any(Permission), expect.any(ConcessionProof), expect.any(FulfilmentRequest) ], undefined ) }) it('does not include a FulfilmentRequest when the permit and contact are not for postal fulfilment', async () => { const mockRecord = mockFinalisedTransactionRecord() mockRecord.permissions[0].permitId = MOCK_1DAY_SENIOR_PERMIT_ENTITY.id docClient.get.mockResolvedValueOnce({ Item: mockRecord }) await processQueue({ id: mockRecord.id }) expect(persist).toBeCalledWith( [ expect.any(Transaction), expect.any(TransactionJournal), expect.any(TransactionJournal), expect.any(Contact), expect.any(Permission), expect.any(ConcessionProof) ], undefined ) }) }) describe('after the fulfilment change date has passed', () => { beforeEach(() => { process.env.FULFILMENT_SWITCHOVER_DATE = moment().subtract(1, 'day').toISOString() }) afterEach(() => { delete process.env.FULFILMENT_SWITCHOVER_DATE }) it('does not include a FulfilmentRequest when the permit and contact are for postal fulfilment', async () => { const mockRecord = mockFinalisedTransactionRecord() mockRecord.permissions[0].permitId = MOCK_12MONTH_SENIOR_PERMIT.id docClient.get.mockResolvedValueOnce({ Item: mockRecord }) await processQueue({ id: mockRecord.id }) expect(persist).toBeCalledWith( [ expect.any(Transaction), expect.any(TransactionJournal), expect.any(TransactionJournal), expect.any(Contact), expect.any(Permission), expect.any(ConcessionProof) ], undefined ) }) it('does not include a FulfilmentRequest when the permit and contact are not for postal fulfilment', async () => { const mockRecord = mockFinalisedTransactionRecord() mockRecord.permissions[0].permitId = MOCK_1DAY_SENIOR_PERMIT_ENTITY.id docClient.get.mockResolvedValueOnce({ Item: mockRecord }) await processQueue({ id: mockRecord.id }) expect(persist).toBeCalledWith( [ expect.any(Transaction), expect.any(TransactionJournal), expect.any(TransactionJournal), expect.any(Contact), expect.any(Permission), expect.any(ConcessionProof) ], undefined ) }) }) it('sets isLicenceForYou to Yes on the transaction, if it is true on the permission', async () => { const mockRecord = mockFinalisedTransactionRecord() mockRecord.permissions[0].isLicenceForYou = true docClient.get.mockResolvedValueOnce({ Item: mockRecord }) await processQueue({ id: mockRecord.id }) const persistMockFirstAgument = persist.mock.calls[0] expect(persistMockFirstAgument[0][4].isLicenceForYou).toBeDefined() expect(persistMockFirstAgument[0][4]).toMatchObject({ isLicenceForYou: { id: 1, label: 'Yes', description: 'Yes' } }) }) it('sets isLicenceForYou to No on the transaction, if it is false on the permission', async () => { const mockRecord = mockFinalisedTransactionRecord() mockRecord.permissions[0].isLicenceForYou = false docClient.get.mockResolvedValueOnce({ Item: mockRecord }) await processQueue({ id: mockRecord.id }) const persistMockFirstAgument = persist.mock.calls[0] expect(persistMockFirstAgument[0][4].isLicenceForYou).toBeDefined() expect(persistMockFirstAgument[0][4]).toMatchObject({ isLicenceForYou: { id: 0, label: 'No', description: 'No' } }) }) it('does not set isLicenceForYou on the transaction, if it is undefined on the permission', async () => { const mockRecord = mockFinalisedTransactionRecord() mockRecord.permissions[0].isLicenceForYou = undefined docClient.get.mockResolvedValueOnce({ Item: mockRecord }) await processQueue({ id: mockRecord.id }) const persistMockFirstAgument = persist.mock.calls[0] expect(persistMockFirstAgument[0][4].isLicenceForYou).toBeUndefined() }) it('does not set isLicenceForYou on the transaction, if it is null on the permission', async () => { const mockRecord = mockFinalisedTransactionRecord() mockRecord.permissions[0].isLicenceForYou = null docClient.get.mockResolvedValueOnce({ Item: mockRecord }) await processQueue({ id: mockRecord.id }) const persistMockFirstAgument = persist.mock.calls[0] expect(persistMockFirstAgument[0][4].isLicenceForYou).toBeUndefined() }) it('handles requests which relate to a transaction file', async () => { const transactionFilename = 'test-file.xml' const mockRecord = mockFinalisedTransactionRecord() mockRecord.transactionFile = transactionFilename docClient.get.mockResolvedValueOnce({ Item: mockRecord }) const transactionToFileBindingSpy = jest.spyOn(Transaction.prototype, 'bindToAlternateKey') const permissionToFileBindingSpy = jest.spyOn(Permission.prototype, 'bindToAlternateKey') await processQueue({ id: mockRecord.id }) expect(transactionToFileBindingSpy).toHaveBeenCalledWith(Transaction.definition.relationships.poclFile, transactionFilename) expect(permissionToFileBindingSpy).toHaveBeenCalledWith(Permission.definition.relationships.poclFile, transactionFilename) }) it('throws 404 not found error if a record cannot be found for the given id', async () => { const mockRecord = mockFinalisedTransactionRecord() docClient.get.mockResolvedValueOnce({ Item: undefined }).mockResolvedValueOnce({ Item: undefined }) try { await processQueue({ id: mockRecord.id }) } catch (e) { expect(e.message).toEqual('A transaction for the specified identifier was not found') expect(e.output.statusCode).toEqual(404) } }) describe.each([20, 38.46, 287])('the provisional transaction amount of £%d is used for final transaction amount', cost => { const setup = async () => { const mockRecord = mockFinalisedTransactionRecord() mockRecord.payment.amount = cost docClient.get.mockResolvedValueOnce({ Item: mockRecord }) await processQueue({ id: mockRecord.id }) const { mock: { calls: [[[transaction, chargeJournal, paymentJournal]]] } } = persist return { transaction, chargeJournal, paymentJournal } } it('for calculating transaction value', async () => { const { transaction } = await setup() expect(transaction.total).toBe(cost) }) it('for calculating chargeJournal value', async () => { const { chargeJournal } = await setup() expect(chargeJournal.total).toBe(cost * -1) }) it('for calculating paymentJournal value', async () => { const { paymentJournal } = await setup() expect(paymentJournal.total).toBe(cost) }) }) describe('recurring payment processing', () => { it('passes transaction record to generateRecurringPaymentRecord', async () => { const callingArgs = {} generateRecurringPaymentRecord.mockImplementationOnce(transaction => { callingArgs.transaction = JSON.parse(JSON.stringify(transaction)) }) const mockRecord = mockFinalisedTransactionRecord() docClient.get.mockResolvedValueOnce({ Item: { ...mockRecord } }) await processQueue({ id: mockRecord.id }) // jest.fn args aren't immutable and transaction is changed in processQueue, so we use our clone that hasn't changed expect(callingArgs.transaction).toEqual(mockRecord) }) it('passes permission to generateRecurringPaymentRecord', async () => { const mockRecord = mockFinalisedTransactionRecord() const expectedPermissionData = {} const keysToCopy = ['referenceNumber', 'issueDate', 'startDate', 'endDate', 'isRenewal'] for (const key of keysToCopy) { expectedPermissionData[key] = mockRecord.permissions[0][key] } docClient.get.mockResolvedValueOnce({ Item: mockRecord }) await processQueue({ id: mockRecord.id }) expect(generateRecurringPaymentRecord).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining(expectedPermissionData)) }) it('passes return value of generateRecurringPaymentRecord to processRecurringPayment', async () => { const rprSymbol = Symbol('rpr') const finalisedTransaction = mockFinalisedTransactionRecord() generateRecurringPaymentRecord.mockReturnValueOnce(rprSymbol) docClient.get.mockResolvedValueOnce({ Item: finalisedTransaction }) await processQueue({ id: finalisedTransaction.id }) expect(processRecurringPayment).toHaveBeenCalledWith(rprSymbol, expect.any(Contact)) }) it('binds existing recurring payment to new recurring payment', async () => { const mockExistingRecurringPayment = { bindToEntity: jest.fn() } const mockNewRecurringPayment = new RecurringPayment() const finalisedTransaction = mockFinalisedTransactionRecord() findNewestExistingRecurringPaymentInCrm.mockReturnValueOnce(mockExistingRecurringPayment) processRecurringPayment.mockReturnValueOnce({ recurringPayment: mockNewRecurringPayment }) docClient.get.mockResolvedValueOnce({ Item: finalisedTransaction }) await processQueue({ id: finalisedTransaction.id }) expect(mockExistingRecurringPayment.bindToEntity).toHaveBeenCalledWith( RecurringPayment.definition.relationships.nextRecurringPayment, mockNewRecurringPayment ) }) }) }) describe('.getTransactionJournalRefNumber', () => { describe('when the transaction type is "Payment"', () => { it.each([ ['123456', 2020], ['654321', 2021], ['567890', 2022] ])("and it's a DDE File, and has a journal id, returns journal id", (journalId, year) => { jest.useFakeTimers() jest.setSystemTime(new Date(year, 1, 1, 10, 0, 0, 0)) const mockRecord = getSampleRecord({ dataSource: DDE_DATA_SOURCE, journalId }) const refNumber = getTransactionJournalRefNumber(mockRecord, 'Payment') expect(refNumber).toBe(`DDE-${year}-${journalId}`) jest.useRealTimers() }) it("and it's a POCL file, and has a journal id and a serial number, returns serial number", () => { const mockRecord = getSampleRecord() mockRecord.journalId = '123456' const refNumber = getTransactionJournalRefNumber(mockRecord, 'Payment') expect(refNumber).toBe(mockRecord.serialNumber) }) it('and the serial number is present, returns serial number', () => { const mockRecord = getSampleRecord() const refNumber = getTransactionJournalRefNumber(mockRecord, 'Payment') expect(refNumber).toBe(mockRecord.serialNumber) }) it('and the serial number is not present, returns id', () => { const mockRecord = getSampleRecord() const refNumber = getTransactionJournalRefNumber({ ...mockRecord, serialNumber: null }, 'Payment') expect(refNumber).toBe(mockRecord.id) }) }) describe('when the transaction type is "Charge"', () => { it('and the serial number is present, returns id', () => { const mockRecord = getSampleRecord() const refNumber = getTransactionJournalRefNumber(mockRecord, 'Charge') expect(refNumber).toBe(mockRecord.id) }) it('and the serial number is not present, returns id', () => { const mockRecord = getSampleRecord() const refNumber = getTransactionJournalRefNumber({ ...mockRecord, serialNumber: null }, 'Charge') expect(refNumber).toBe(mockRecord.id) }) }) const getSampleRecord = (overrides = {}) => ({ ...mockFinalisedTransactionRecord(), dataSource: POCL_DATA_SOURCE, ...overrides }) }) })