Skip to content

Commit

Permalink
23509 23731 - Cancel, refresh, redirect on cc payment events (#283)
Browse files Browse the repository at this point in the history
* 23509 23731 - feature: add ability to cancel document request cc payment. redirect on successful payment.

* 23509 23731 - feature: API add ability to cancel document request cc payment.

* Flake8

* PR updates & code cleanup.
  • Loading branch information
hfekete authored Nov 8, 2024
1 parent edf7ce8 commit 9c67d82
Show file tree
Hide file tree
Showing 19 changed files with 416 additions and 45 deletions.
32 changes: 32 additions & 0 deletions search-api/migrations/versions/20241105_164024_0ba810e98e41_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""empty message
Revision ID: 0ba810e98e41
Revises: eaf25b9a20bf
Create Date: 2024-11-05 16:40:24.716461
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '0ba810e98e41'
down_revision = 'eaf25b9a20bf'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('document_access_request', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_document_access_request_payment_id'), ['payment_id'], unique=False)

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('document_access_request', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_document_access_request_payment_id'))

# ### end Alembic commands ###
8 changes: 6 additions & 2 deletions search-api/src/search_api/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,13 @@ def __post_init__(self):
class DbRecordNotFoundException(BaseExceptionE):
"""Row not found in database."""

def __init__(self):
def __init__(self, message=None):
"""Return a valid Record Not Found Exception."""
self.message = 'DB record not found'
if message is None:
self.message = 'DB record not found'
else:
self.message = message

self.status_code = HTTPStatus.NOT_FOUND
super().__init__()

Expand Down
24 changes: 23 additions & 1 deletion search-api/src/search_api/models/document_access_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from datetime import datetime, timezone
from enum import auto
from http import HTTPStatus
from typing import List

from sqlalchemy import inspect
from sqlalchemy.ext.hybrid import hybrid_property
Expand Down Expand Up @@ -47,7 +48,7 @@ class Status(BaseEnum):
status = db.Column('status', db.Enum(Status), default=Status.CREATED)
account_id = db.Column('account_id', db.Integer)
_payment_status_code = db.Column('payment_status_code', db.String(50))
_payment_token = db.Column('payment_id', db.String(4096))
_payment_token = db.Column('payment_id', db.String(4096), index=True)
_payment_completion_date = db.Column('payment_completion_date', db.DateTime(timezone=True))
submission_date = db.Column(db.DateTime(timezone=True), default=datetime.utcnow)
expiry_date = db.Column(db.DateTime(timezone=True))
Expand Down Expand Up @@ -158,13 +159,34 @@ def find_by_id(cls, request_id: int) -> DocumentAccessRequest:
"""Return a request having the specified id."""
return cls.query.filter_by(id=request_id).one_or_none()

@classmethod
def find_by_payment_token(cls, payment_id: str) -> List[DocumentAccessRequest]:
"""Return a list of requests having the specified payment_id."""
return cls.query.filter_by(_payment_token=payment_id).all()

@staticmethod
def _raise_default_lock_exception():
raise BusinessException(
error='Request cannot be modified after the invoice is created.',
status_code=HTTPStatus.FORBIDDEN
)

def cancel(self):
"""
Cancel the current payment request if it is in the CREATED state.
Raises a BusinessException with an error and the HTTP status FORBIDDEN
if the payment request is not in the CREATED state.
"""
if self.status != self.Status.CREATED:
raise BusinessException(
error='Payment can be only cancelled if in state created.',
status_code=HTTPStatus.FORBIDDEN
)
self.status = DocumentAccessRequest.status = self.Status.ERROR
self._payment_status_code = 'PAYMENT_CANCELLED'
self.save()

def save(self):
"""Store the request into the db."""
db.session.add(self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def create_invoice(document_access_request: DocumentAccessRequest, user_jwt: Jwt
today_utc = datetime.now()

document_access_request.payment_token = pid
document_access_request.submission_date = today_utc
if is_pad:
document_access_request.status = DocumentAccessRequest.Status.PAID
document_access_request.payment_completion_date = today_utc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,28 @@ def post(business_identifier): # pylint: disable=too-many-return-statements
except Exception as default_exception: # noqa: B902
current_app.logger.error(default_exception.with_traceback(None))
return resource_utils.default_exception_response(default_exception)


@bp.delete('/<int:request_id>')
@cross_origin(origin='*')
@jwt.requires_auth
def cancel_request(business_identifier, request_id):
"""Cancel document access request by id if it is in state created."""
try:
account_id = request.headers.get('Account-Id', None)
if not account_id:
return resource_utils.account_required_response()

access_request = DocumentAccessRequest.find_by_id(request_id)

if not access_request or access_request.business_identifier != business_identifier:
return resource_utils.not_found_error_response('Document Access Request', request_id)
if str(access_request.account_id) != account_id:
return resource_utils.unauthorized_error_response(account_id)

access_request.cancel()

return {}, HTTPStatus.OK

except Exception as default_exception: # noqa: B902
return resource_utils.default_exception_response(default_exception)
12 changes: 8 additions & 4 deletions search-api/src/search_api/resources/v2/payments/payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,16 @@ def gcp_listener():
credit_card_payment = ce.data
if credit_card_payment.get('corpTypeCode', '') != 'BUS':
raise Exception('invalid or missing corpTypeCode.') # noqa: E713 # pylint: disable=broad-exception-raised
dar = DocumentAccessRequest.find_by_id(credit_card_payment['id'])
if not dar:
payment_id = credit_card_payment['id']

dars = DocumentAccessRequest.find_by_payment_token(str(payment_id))

if dars is None:
raise DbRecordNotFoundException()

dar.status = credit_card_payment['statusCode']
dar.save()
for dar in dars:
dar.status = credit_card_payment['statusCode']
dar.save()

return {}, HTTPStatus.OK
except Exception: # noqa pylint: disable=broad-except
Expand Down
63 changes: 55 additions & 8 deletions search-ui/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,42 @@

<script setup lang="ts">
// External
import { computed, onMounted, ref, watch, Ref } from 'vue'
import { computed, onMounted, ref, Ref, watch } from 'vue'
import * as Sentry from '@sentry/vue'
import { useRoute, useRouter } from 'vue-router'
import { StatusCodes } from 'http-status-codes'
// BC Registry
import { SessionStorageKeys } from 'sbc-common-components/src/util/constants'
import { LoadingScreen, SbcFooter, SbcHeader } from '@/sbc-common-components'
// Bcrs shared components
import { BreadcrumbIF } from '@/interfaces'
import { BreadcrumbIF, DialogOptionsI, ErrorI } from '@/interfaces'
// Local
import { ErrorCategories, ErrorCodes, ProductCode, RouteNames } from '@/enums'
import { DialogOptionsI, ErrorI } from '@/interfaces'
import { BcrsBreadcrumb } from '@/bcrs-common-components'
import { BaseDialog, EntityInfo } from '@/components'
import { useAuth, useEntity, useFeeCalculator, useFilingHistory, useSearch, useSuggest,
useDocumentAccessRequest } from '@/composables'
import { AuthAccessError, DefaultError, EntityLoadError, PayDefaultError, PayBcolError,
PayPadError, ReportError } from '@/resources/error-dialog-options'
import {
useAuth,
useDocumentAccessRequest,
useEntity,
useFeeCalculator,
useFilingHistory,
useSearch,
useSuggest
} from '@/composables'
import {
AuthAccessError,
DefaultError,
EntityLoadError,
PayBcolError,
PayDefaultError,
PayPadError,
ReportError
} from '@/resources/error-dialog-options'
import { HelpdeskInfo } from '@/resources/contact-info'
import { getFeatureFlag } from '@/utils'
import ContactInfo from './components/common/ContactInfo.vue'
import { DocumentAccessRequestStatus } from '@/enums/document-access-request'
import { PaymentCancelledError } from '@/resources/error-dialog-options/payment-canceled'
const aboutText: string = 'Search UI v' + process.env.VUE_APP_VERSION
const appLoading = ref(false)
Expand All @@ -89,7 +104,8 @@ const { filingHistory } = useFilingHistory()
const { fees } = useFeeCalculator()
const { search } = useSearch()
const { suggest } = useSuggest()
const { documentAccessRequest, loadAccessRequestHistory } = useDocumentAccessRequest()
const { cancelAccessRequest, documentAccessRequest, loadAccessRequestHistory, getAccessRequestById }
= useDocumentAccessRequest()
/** True if Jest is running the code. */
const isJestRunning = computed((): boolean => {
Expand Down Expand Up @@ -155,6 +171,31 @@ onMounted(async () => {
name: RouteNames.BUSINESS_INFO,
params: { identifier: route.query?.identifier }
})
} else if (route.query?.documentAccessRequestId) {
const darId = Number(route.query?.documentAccessRequestId)
let currentDar = await getAccessRequestById(darId)
if (currentDar?.status === DocumentAccessRequestStatus.CREATED &&
route.query?.status === 'UEFZTUVOVF9DQU5DRUxMRUQ=' // this is PAYMENT_CANCELLED
) {
await cancelAccessRequest(entity, currentDar.id)
currentDar = await getAccessRequestById(darId)
} else if (currentDar?.status === DocumentAccessRequestStatus.CREATED) {
// wait 10 seconds to see if the document gets into the paid state
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 1000))
currentDar = await getAccessRequestById(darId)
if (currentDar.status !== DocumentAccessRequestStatus.CREATED) {
break
}
}
}
if (currentDar?.status === DocumentAccessRequestStatus.COMPLETED) {
router.push({ name: RouteNames.DOCUMENT_REQUEST, params: { darId } })
}
documentAccessRequest.currentRequest = currentDar
} else if (route.query?.docAccessId) {
await loadedHistory
documentAccessRequest.currentRequest = documentAccessRequest.requests
Expand Down Expand Up @@ -258,8 +299,14 @@ const handleError = (error: ErrorI) => {
Sentry.captureException(error)
break
case ErrorCategories.SEARCH_UNAVAILABLE:
case ErrorCategories.DOCUMENT_ACCESS_PAYMENT_ERROR:
// handled inline and no error msg needed
break
case ErrorCategories.DOCUMENT_ACCESS_PAYMENT_CANCELLED:
errorInfo.value = {...PaymentCancelledError}
errorContactInfo.value = false
errorDisplay.value = true
break
default:
errorInfo.value = {...DefaultError}
errorContactInfo.value = true
Expand Down
45 changes: 45 additions & 0 deletions search-ui/src/components/common/ErrorBox.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { ContactInfo } from '@/components'
import { BcRegistries } from '@/resources/contact-info'
defineProps<{
title: string
errorText: string
hasBcRegContact: { type: string, required: false, default: false }
}>()
</script>

<template>

<div class="errorBox">
<div class="content">
<v-icon color="red">mdi-alert</v-icon>
<span class="title">{{ title }}:</span>
<span class="errorText">{{ errorText }}</span>
</div>
<div class="contactInfo">
<contact-info v-if="hasBcRegContact" class="font-normal font-16 mt-4" :contacts="BcRegistries" />
</div>
</div>
</template>

<style scoped lang="scss">
.errorBox {
border: 1px solid #d3272c;
background-color: #FAE9E9;
padding: 16px;
}
.content {
padding-bottom: 4px;
}
.title {
font-weight: bold;
padding: 4px;
}
.errorText {
font-weight: normal;
}
</style>
Loading

0 comments on commit 9c67d82

Please sign in to comment.