diff --git a/search-api/src/search_api/models/document_access_request.py b/search-api/src/search_api/models/document_access_request.py index 876695b4..cfc7254b 100644 --- a/search-api/src/search_api/models/document_access_request.py +++ b/search-api/src/search_api/models/document_access_request.py @@ -126,7 +126,8 @@ def json(self): 'submissionDate': self.submission_date.isoformat(), 'expiryDate': self.expiry_date.isoformat() if self.expiry_date else None, 'outputFileKey': self._output_file_key, - 'submitter': self.submitter.display_name if self.submitter else None + 'submitter': self.submitter.display_name if self.submitter else None, + 'paymentToken': self.payment_token # invoice id from the pay db } documents = [] diff --git a/search-api/src/search_api/request_handlers/document_access_request_handler.py b/search-api/src/search_api/request_handlers/document_access_request_handler.py index 3dad1701..46208148 100644 --- a/search-api/src/search_api/request_handlers/document_access_request_handler.py +++ b/search-api/src/search_api/request_handlers/document_access_request_handler.py @@ -70,18 +70,22 @@ def create_invoice(document_access_request: DocumentAccessRequest, user_jwt: Jwt header, business_json) if payment_response.status_code in (HTTPStatus.OK, HTTPStatus.CREATED): - payment_completion_date = datetime.utcnow() + is_pad = payment_response.json().get('paymentMethod') == 'PAD' pid = payment_response.json().get('id') + today_utc = datetime.now() + document_access_request.payment_token = pid + if is_pad: + document_access_request.status = DocumentAccessRequest.Status.PAID + document_access_request.payment_completion_date = today_utc + else: + document_access_request.status = DocumentAccessRequest.Status.CREATED document_access_request.payment_status_code = payment_response.json().get('statusCode', '') - document_access_request.payment_completion_date = payment_completion_date validity_in_days = current_app.config.get('DOCUMENT_REQUEST_VALIDITY_DURATION', 14) - document_access_request.expiry_date = payment_completion_date + relativedelta(days=validity_in_days) - document_access_request.status = DocumentAccessRequest.Status.PAID + document_access_request.expiry_date = today_utc + relativedelta(days=validity_in_days) document_access_request.save() return {'isPaymentActionRequired': payment_response.json().get('isPaymentActionRequired', False)}, HTTPStatus.CREATED - if payment_response.status_code == HTTPStatus.BAD_REQUEST: # Set payment error type used to retrieve error messages from pay-api error_type = payment_response.json().get('type') diff --git a/search-api/src/search_api/services/validator.py b/search-api/src/search_api/services/validator.py index 3cb457c3..b411803b 100644 --- a/search-api/src/search_api/services/validator.py +++ b/search-api/src/search_api/services/validator.py @@ -37,10 +37,6 @@ def validate_document_access_request(document_access_request_json: dict, account if not account_org: validation_errors.append({'error': 'Invalid Account'}) - if account_org.get('orgType') != 'PREMIUM': - validation_errors.append({ - 'error': 'Document Access Request can be created only by a premium account user'}) - documents = document_access_request_json.get('documentAccessRequest', {}).get('documents', []) if not documents: validation_errors.append({'error': 'Document list must contain atleast one document type'}) diff --git a/search-api/tests/unit/api/businesses/test_document_request.py b/search-api/tests/unit/api/businesses/test_document_request.py index 89eafb38..0e998e8e 100644 --- a/search-api/tests/unit/api/businesses/test_document_request.py +++ b/search-api/tests/unit/api/businesses/test_document_request.py @@ -23,11 +23,10 @@ from search_api.models import Document, DocumentAccessRequest, User from search_api.services import queue from search_api.services.authz import STAFF_ROLE -from search_api.services.validator import RequestValidator from search_api.services.flags import Flags from tests.unit import MockResponse -from tests.unit.services.utils import create_header, helper_create_jwt +from tests.unit.services.utils import create_header DOCUMENT_ACCESS_REQUEST_TEMPLATE = { @@ -233,7 +232,7 @@ def create_user(): def _create_user(**kwargs): if not kwargs: return User() - + return User(**kwargs) return _create_user @@ -256,7 +255,7 @@ def test_post_business_document_submit_ce_to_queue(ld, session, client, jwt, moc idp_userid = '123' iss = 'iss' login_source = 'API_GW' - + mocker.patch('search_api.services.validator.RequestValidator.validate_document_access_request', return_value=[]) mocker.patch('search_api.resources.v1.businesses.documents.document_request.get_role', @@ -287,7 +286,7 @@ def test_post_business_document_submit_ce_to_queue(ld, session, client, jwt, moc HTTPStatus.OK) mocker.patch('search_api.resources.v1.businesses.documents.document_request.get_business', return_value=business_mock_response) - + mock_pub = mocker.patch.object(queue, 'publish', return_value=[]) # set the test data for the flag @@ -298,7 +297,7 @@ def test_post_business_document_submit_ce_to_queue(ld, session, client, jwt, moc .variations(False, True) .variation_for_user(flag_user['key'], flag_value) .fallthrough_variation(False)) - + # Test api_response = client.post(f'/api/v1/businesses/{business_identifier}/documents/requests', data=json.dumps(DOCUMENT_ACCESS_REQUEST_TEMPLATE), diff --git a/search-api/tests/unit/mocks/create-invoice-cc-response.json b/search-api/tests/unit/mocks/create-invoice-cc-response.json new file mode 100644 index 00000000..3ece0700 --- /dev/null +++ b/search-api/tests/unit/mocks/create-invoice-cc-response.json @@ -0,0 +1,57 @@ +{ + "_links": { + "collection": "/api/v1/payment-requests?invoice_id=41861", + "self": "/api/v1/payment-requests/41861" + }, + "businessIdentifier": "FM1000026", + "corpTypeCode": "BUS", + "createdBy": "BCSC/ZGCBNHM6U7FPRXI7AJT6KFF7GSKY43TA", + "createdName": "BCREG2 Ayisha FIFTY", + "createdOn": "2024-10-25T21:19:17+00:00", + "details": [ + { + "label": "Registration Number: ", + "value": "FM1000026" + } + ], + "id": 41861, + "isPaymentActionRequired": true, + "lineItems": [ + { + "description": "Business Summary", + "filingFees": 7.0, + "futureEffectiveFees": 0.0, + "gst": 0.0, + "id": 44464, + "priorityFees": 0.0, + "pst": 0.0, + "quantity": 1, + "serviceFees": 1.5, + "statusCode": "ACTIVE", + "total": 8.5, + "waivedBy": null, + "waivedFees": 0.0 + } + ], + "overdueDate": "2024-12-15T08:00:00+00:00", + "paid": 0.0, + "paymentAccount": { + "accountId": "3137", + "accountName": "Kial Dev 3 (BTR test account) - Public search access", + "billable": true, + "branchName": "Public search access" + }, + "paymentMethod": "DIRECT_PAY", + "references": [ + { + "createdOn": "2024-10-25T21:19:17+00:00", + "id": 34456, + "invoiceNumber": "REGUT00041861", + "statusCode": "ACTIVE" + } + ], + "refund": 0.0, + "serviceFees": 1.5, + "statusCode": "CREATED", + "total": 8.5 +} diff --git a/search-api/tests/unit/mocks/create-invoice-pad-response.json b/search-api/tests/unit/mocks/create-invoice-pad-response.json new file mode 100644 index 00000000..e75855a6 --- /dev/null +++ b/search-api/tests/unit/mocks/create-invoice-pad-response.json @@ -0,0 +1,49 @@ +{ + "_links": { + "collection": "/api/v1/payment-requests?invoice_id=41863", + "self": "/api/v1/payment-requests/41863" + }, + "businessIdentifier": "FM1000026", + "corpTypeCode": "BUS", + "createdBy": "BCSC/ZGCBNHM6U7FPRXI7AJT6KFF7GSKY43TA", + "createdName": "BCREG2 Ayisha FIFTY", + "createdOn": "2024-10-25T21:22:37+00:00", + "details": [ + { + "label": "Registration Number: ", + "value": "FM1000026" + } + ], + "id": 41863, + "isPaymentActionRequired": false, + "lineItems": [ + { + "description": "Business Summary", + "filingFees": 7.0, + "futureEffectiveFees": 0.0, + "gst": 0.0, + "id": 44466, + "priorityFees": 0.0, + "pst": 0.0, + "quantity": 1, + "serviceFees": 1.5, + "statusCode": "ACTIVE", + "total": 7.0, + "waivedBy": null, + "waivedFees": 0.0 + } + ], + "overdueDate": "2024-12-15T08:00:00+00:00", + "paid": 0.0, + "paymentAccount": { + "accountId": "3101", + "accountName": "Kial Dev 2 (BTR test account)-Director search access", + "billable": true, + "branchName": "Director search access" + }, + "paymentMethod": "PAD", + "refund": 0.0, + "serviceFees": 1.5, + "statusCode": "APPROVED", + "total": 8.5 +} diff --git a/search-api/tests/unit/models/test_document_access_request.py b/search-api/tests/unit/models/test_document_access_request.py index fdbf195e..e66d906e 100644 --- a/search-api/tests/unit/models/test_document_access_request.py +++ b/search-api/tests/unit/models/test_document_access_request.py @@ -139,6 +139,7 @@ def test_document_access_request_json(session): 'id': document_access_request.id, 'outputFileKey': None, 'paymentStatus': 'COMPLETED', + 'paymentToken': document_access_request.payment_token, 'status': 'PAID', 'submissionDate': document_access_request.submission_date.isoformat(), 'submitter': 'firstname lastname' diff --git a/search-api/tests/unit/request_handlers/test_document_access_request_handler.py b/search-api/tests/unit/request_handlers/test_document_access_request_handler.py index fa7eb401..17946e86 100644 --- a/search-api/tests/unit/request_handlers/test_document_access_request_handler.py +++ b/search-api/tests/unit/request_handlers/test_document_access_request_handler.py @@ -17,14 +17,14 @@ from datetime import datetime from http import HTTPStatus +import pytest from flask import current_app, g from search_api.models import DocumentAccessRequest, User from search_api.request_handlers.document_access_request_handler import create_invoice, save_request - DOCUMENT_ACCESS_REQUEST_TEMPLATE = { - "documentAccessRequest":{ + "documentAccessRequest": { "documents": [ { "type": "BUSINESS_SUMMARY_FILING_HISTORY" @@ -33,9 +33,10 @@ } } + def test_save_request(client, session, jwt, mocker): """Assert that request can be saved.""" - g.jwt_oidc_token_info={} + g.jwt_oidc_token_info = {} user = User(username='username', firstname='firstname', lastname='lastname', sub='sub', iss='iss', idp_userid='123') user.save() mocker.patch('search_api.models.User.get_or_create_user_by_jwt', return_value=user) @@ -45,12 +46,15 @@ def test_save_request(client, session, jwt, mocker): assert document_access_request.submitter.firstname == user.firstname -def test_create_invoice(client, session, jwt, mocker): +@pytest.mark.parametrize('test_name,mock_response,is_payment_completion_date_expected', [ + ('test_pad_invoice', {'id': 123, 'paymentMethod': 'PAD'}, True), + ('test_pad_invoice', {'id': 123, 'paymentMethod': 'DIRECT_PAY'}, False)]) +def test_create_invoice(client, session, jwt, mocker, test_name, mock_response, is_payment_completion_date_expected): """Assert that access request is updated with payment details.""" document_access_request = DocumentAccessRequest( business_identifier='CP1234567', account_id=123, - submission_date=datetime.utcnow() + submission_date=datetime.now() ) document_access_request.save() @@ -64,9 +68,9 @@ def test_create_invoice(client, session, jwt, mocker): } } - mock_response = MockResponse({'id': 123},HTTPStatus.CREATED) + mock_payment_response = MockResponse(mock_response, HTTPStatus.CREATED) mocker.patch('search_api.request_handlers.document_access_request_handler.create_payment', - return_value=mock_response) + return_value=mock_payment_response) mocker.patch('search_api.request_handlers.document_access_request_handler.get_role', return_value='basic') @@ -74,8 +78,12 @@ def test_create_invoice(client, session, jwt, mocker): create_invoice(document_access_request, jwt, request_json, business_json) document_access_request = DocumentAccessRequest.find_by_id(document_access_request.id) + assert document_access_request.payment_token - assert document_access_request.payment_completion_date + if is_payment_completion_date_expected: + assert document_access_request.payment_completion_date + else: + assert not document_access_request.payment_completion_date assert document_access_request.expiry_date @@ -95,7 +103,7 @@ def test_create_invoice_failure(client, session, jwt, mocker): 'documentAccessRequest': document_access_request } - mock_response = MockResponse({'type': 'BAD_REQUEST'},HTTPStatus.BAD_REQUEST) + mock_response = MockResponse({'type': 'BAD_REQUEST'}, HTTPStatus.BAD_REQUEST) mocker.patch('search_api.request_handlers.document_access_request_handler.create_payment', return_value=mock_response) mocker.patch('search_api.request_handlers.document_access_request_handler.get_role', diff --git a/search-api/tests/unit/services/test_validator.py b/search-api/tests/unit/services/test_validator.py index 4db76cb6..0125a5c9 100644 --- a/search-api/tests/unit/services/test_validator.py +++ b/search-api/tests/unit/services/test_validator.py @@ -71,23 +71,6 @@ def test_document_access_request_valid(client, session, jwt, requests_mock): assert err is None -def test_document_access_request_invalid_basic_account(client, session, jwt, requests_mock): - """Assert that a auth-api user orgs request works as expected with the mock service endpoint.""" - # setup - current_app.config.update(AUTH_SVC_URL=MOCK_URL_NO_KEY) - token = helper_create_jwt(jwt, [authz.PPR_ROLE]) - USERS_ORG_COPY = copy.deepcopy(USERS_ORG) - USERS_ORG_COPY['orgs'][0]['orgType'] = 'BASIC' - org = USERS_ORG_COPY['orgs'][0] - requests_mock.get(f"{current_app.config.get('AUTH_SVC_URL')}users/orgs", json=USERS_ORG_COPY) - requests_mock.get(f"{current_app.config.get('AUTH_SVC_URL')}orgs/{org['id']}", json=org) - - err =RequestValidator.validate_document_access_request(DOCUMENT_ACCESS_REQUEST_TEMPLATE, org['id'], token, 'basic') - # check - - assert err[0]['error'] == 'Document Access Request can be created only by a premium account user' - - @pytest.mark.parametrize('test_name, error_message', [ ('no_documents', 'Document list must contain atleast one document type'), ('invalid_document_type', 'Invalid Document Type') diff --git a/search-ui/src/composables/document-access-request-factory.ts b/search-ui/src/composables/document-access-request-factory.ts index 08f34146..d247e8fa 100644 --- a/search-ui/src/composables/document-access-request-factory.ts +++ b/search-ui/src/composables/document-access-request-factory.ts @@ -1,11 +1,17 @@ import { reactive } from 'vue' import { DocumentType } from '@/enums' -import { StaffPaymentIF } from '@/interfaces' -import { AccessRequestsHistoryI, DocumentAccessRequestsI, CreateDocumentResponseI, DocumentI } from '@/interfaces' +import { + AccessRequestsHistoryI, + CreateDocumentResponseI, + DocumentAccessRequestsI, + DocumentI, + StaffPaymentIF +} from '@/interfaces' import { EntityI } from '@/interfaces/entity' -import { getActiveAccessRequests, createDocumentAccessRequest, getDocument, fetchFilingDocument } from '@/requests' -import { Document } from '@/types' +import { createDocumentAccessRequest, fetchFilingDocument, getActiveAccessRequests, getDocument } from '@/requests' +import { Document } from '@/types' +import { DocumentAccessRequestPaymentStatus } from '@/enums/document-access-request-payment-status' const documentAccessRequest = reactive({ @@ -14,7 +20,8 @@ const documentAccessRequest = reactive({ _error: null, _loading: false, _saving: false, - _downloading: false + _downloading: false, + _needsPayment: false }) as DocumentAccessRequestsI export const useDocumentAccessRequest = () => { @@ -47,24 +54,27 @@ export const useDocumentAccessRequest = () => { documentAccessRequest._error = response.error } else { documentAccessRequest.currentRequest = response.createDocumentResponse - } + if(documentAccessRequest.currentRequest.paymentStatus === DocumentAccessRequestPaymentStatus.CREATED) { + documentAccessRequest._needsPayment = true + } + } documentAccessRequest._saving = false } const downloadDocument = async (businessIdentifier: string, document: DocumentI) => { documentAccessRequest._downloading = true documentAccessRequest._error = null - const response = await getDocument(businessIdentifier, document) + const response = await getDocument(businessIdentifier, document) if (response?.error){ documentAccessRequest._error = response.error - } + } documentAccessRequest._downloading = false } - const downloadFilingDocument = async (businessIdentifier: string, filingId: number, document: Document) => { + const downloadFilingDocument = async (businessIdentifier: string, filingId: number, document: Document) => { documentAccessRequest._downloading = true documentAccessRequest._error = null - const response = await fetchFilingDocument(businessIdentifier, filingId, document) + const response = await fetchFilingDocument(businessIdentifier, filingId, document) if (response?.error){ documentAccessRequest._error = response.error } diff --git a/search-ui/src/enums/document-access-request-payment-status.ts b/search-ui/src/enums/document-access-request-payment-status.ts new file mode 100644 index 00000000..b0079b2f --- /dev/null +++ b/search-ui/src/enums/document-access-request-payment-status.ts @@ -0,0 +1,4 @@ +export enum DocumentAccessRequestPaymentStatus { + CREATED = 'CREATED', + PAID = 'PAID' +} diff --git a/search-ui/src/interfaces/document-request-interface.ts b/search-ui/src/interfaces/document-request-interface.ts index 5be465f5..45fb10ac 100644 --- a/search-ui/src/interfaces/document-request-interface.ts +++ b/search-ui/src/interfaces/document-request-interface.ts @@ -1,4 +1,5 @@ -import { ErrorI } from '@/interfaces' +import { ErrorI } from '@/interfaces' +import { DocumentAccessRequestPaymentStatus } from '@/enums/document-access-request-payment-status' export interface DocumentDetailsI { businessIdentifier: string, @@ -10,32 +11,34 @@ export interface DocumentDetailsI { expiryDate: string, submitter: string, documents: DocumentI[] + paymentStatus: DocumentAccessRequestPaymentStatus + paymentToken: string } // api responses -export interface CreateDocumentResponseI { +export interface CreateDocumentResponseI { createDocumentResponse?: DocumentDetailsI, error?: ErrorI } -export interface DocumentI { +export interface DocumentI { documentKey: string, documentType: string, fileName: string, id: number } -export interface AccessRequestsHistoryI { +export interface AccessRequestsHistoryI { documentAccessRequests?: DocumentDetailsI[], error?: ErrorI } -export interface DocumentAccessRequestsI { +export interface DocumentAccessRequestsI { requests: DocumentDetailsI[], currentRequest: DocumentDetailsI, _error: ErrorI, _loading: boolean, _saving: boolean, - _downloading: boolean + _downloading: boolean, + _needsPayment: boolean } - diff --git a/search-ui/src/utils/navigate.ts b/search-ui/src/utils/navigate.ts index 2f5604a4..98898061 100644 --- a/search-ui/src/utils/navigate.ts +++ b/search-ui/src/utils/navigate.ts @@ -23,3 +23,17 @@ export function navigate (url: string): boolean { return false } } + +/** + * Navigates to the direct payment URL (credit card payment processing) + */ +export const redirectToPayment = async (invoiceId: string, documentAccessRequestId: string) => { + const currentUrl = new URL(window.location.origin + window.location.pathname) + currentUrl.searchParams.append('documentAccessRequestId', documentAccessRequestId) + const encodedURI = encodeURIComponent(currentUrl.href) + + const paymentUrl = sessionStorage.getItem('AUTH_WEB_URL') + 'makepayment' + const directPayUrl = paymentUrl + '/' + invoiceId + '/' + encodedURI + + window.location.assign(directPayUrl) +} diff --git a/search-ui/src/views/BusinessInfoView.vue b/search-ui/src/views/BusinessInfoView.vue index 737bcd5a..bf7bb3a4 100644 --- a/search-ui/src/views/BusinessInfoView.vue +++ b/search-ui/src/views/BusinessInfoView.vue @@ -193,6 +193,7 @@ import { useAuth, useEntity, useFeeCalculator, useFilingHistory, useDocumentAcce import { ActionComps, FeeCodes, FeeEntities, RouteNames, DocumentType } from '@/enums' import { FeeAction, FeeI, FeeDataI, DialogOptionsI } from '@/interfaces' import { RegistriesInfo } from '@/resources/contact-info' +import { redirectToPayment } from '@/utils' const props = defineProps({ appReady: { default: false }, @@ -278,6 +279,10 @@ const payForDocuments = async () => { await loadAccessRequestHistory() router.push({ name: RouteNames.DOCUMENT_REQUEST, params: { identifier: entity.identifier } }) } + if (documentAccessRequest._needsPayment) { + const currentDar = documentAccessRequest.currentRequest + await redirectToPayment(currentDar.paymentToken, currentDar.id.toString()) + } loading.value = false } @@ -435,21 +440,21 @@ const toggleFee = (event: any, item: any) => { &__label, &__fee { - color: $gray8; + color: $gray8; } - &__label { + &__label { width: 85%; } - &__fee { + &__fee { text-align: right; width: 15%; margin-right: 5px; } } -.document-list-error { +.document-list-error { border-left: solid 4px #D3272C; border-top-right-radius: 5px; border-bottom-right-radius: 5px;