Skip to content

Commit

Permalink
WIP refactor schema
Browse files Browse the repository at this point in the history
  • Loading branch information
steventux committed Jan 21, 2025
1 parent dfdcfe4 commit 442e062
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 28 deletions.
6 changes: 3 additions & 3 deletions alembic/versions/055d1846f547_create_message_statuses.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@

def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
sa.Enum('CREATED', 'PENDING_ENRICHMENT', 'ENRICHED', 'SENDING', 'DELIVERED', 'FAILED', name='messagestatuses').create(op.get_bind())
sa.Enum('created', 'pending_enrichment', 'enriched', 'sending', 'delivered', 'failed', name='messagestatuses').create(op.get_bind())
op.create_table('message_statuses',
sa.Column('created_at', sa.TIMESTAMP(), nullable=False),
sa.Column('details', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('idempotency_key', sa.Text(), nullable=False),
sa.Column('message_id', sa.String(), nullable=True),
sa.Column('message_reference', sa.UUID(), nullable=False),
sa.Column('status', postgresql.ENUM('CREATED', 'PENDING_ENRICHMENT', 'ENRICHED', 'SENDING', 'DELIVERED', 'FAILED', name='messagestatuses', create_type=False), nullable=True),
sa.Column('status', postgresql.ENUM('created', 'pending_enrichment', 'enriched', 'sending', 'delivered', 'failed', name='messagestatuses', create_type=False), nullable=True),
sa.PrimaryKeyConstraint('idempotency_key')
)
# ### end Alembic commands ###
Expand All @@ -36,5 +36,5 @@ def upgrade() -> None:
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('message_statuses')
sa.Enum('CREATED', 'PENDING_ENRICHMENT', 'ENRICHED', 'SENDING', 'DELIVERED', 'FAILED', name='messagestatuses').drop(op.get_bind())
sa.Enum('created', 'pending_enrichment', 'enriched', 'sending', 'delivered', 'failed', name='messagestatuses').drop(op.get_bind())
# ### end Alembic commands ###
6 changes: 3 additions & 3 deletions alembic/versions/0a8c7e489eba_create_channel_statuses.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@

def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
sa.Enum('DELIVERED', 'READ', 'NOTIFICATION_ATTEMPTED', 'UNNOTIFIED', 'REJECTED', 'NOTIFIED', 'RECEIVED', 'PERMANENT_FAILURE', 'TEMPORARY_FAILURE', 'TECHNICAL_FAILURE', name='channelstatuses').create(op.get_bind())
sa.Enum('delivered', 'read', 'notification_attempted', 'unnotified', 'rejected', 'notified', 'received', 'permanent_failure', 'temporary_failure', 'technical_failure', name='channelstatuses').create(op.get_bind())
op.create_table('channel_statuses',
sa.Column('created_at', sa.TIMESTAMP(), nullable=False),
sa.Column('details', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('idempotency_key', sa.Text(), nullable=False),
sa.Column('message_id', sa.String(), nullable=True),
sa.Column('message_reference', sa.UUID(), nullable=False),
sa.Column('status', postgresql.ENUM('DELIVERED', 'READ', 'NOTIFICATION_ATTEMPTED', 'UNNOTIFIED', 'REJECTED', 'NOTIFIED', 'RECEIVED', 'PERMANENT_FAILURE', 'TEMPORARY_FAILURE', 'TECHNICAL_FAILURE', name='channelstatuses', create_type=False), nullable=True),
sa.Column('status', postgresql.ENUM('delivered', 'read', 'notification_attempted', 'unnotified', 'rejected', 'notified', 'received', 'permanent_failure', 'temporary_failure', 'technical_failure', name='channelstatuses', create_type=False), nullable=True),
sa.PrimaryKeyConstraint('idempotency_key')
)
# ### end Alembic commands ###
Expand All @@ -36,5 +36,5 @@ def upgrade() -> None:
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('channel_statuses')
sa.Enum('DELIVERED', 'READ', 'NOTIFICATION_ATTEMPTED', 'UNNOTIFIED', 'REJECTED', 'NOTIFIED', 'RECEIVED', 'PERMANENT_FAILURE', 'TEMPORARY_FAILURE', 'TECHNICAL_FAILURE', name='channelstatuses').drop(op.get_bind())
sa.Enum('delivered', 'read', 'notification_attempted', 'unnotified', 'rejected', 'notified', 'received', 'permanent_failure', 'temporary_failure', 'technical_failure', name='channelstatuses').drop(op.get_bind())
# ### end Alembic commands ###
8 changes: 4 additions & 4 deletions alembic/versions/44c6fa94f173_create_messagebatch_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@

def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
sa.Enum('NOT_SENT', 'SENT', 'FAILED', name='messagebatchstatuses').create(op.get_bind())
sa.Enum('not_sent', 'sent', 'failed', name='messagebatchstatuses').create(op.get_bind())
op.create_table('message_batches',
sa.Column('batch_id', sa.UUID(), nullable=False),
sa.Column('batch_id', sa.Text(), nullable=False, default='UNKNOWN'),
sa.Column('batch_reference', sa.UUID(), nullable=False),
sa.Column('created_at', sa.TIMESTAMP(), nullable=False),
sa.Column('details', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('status', postgresql.ENUM('NOT_SENT', 'SENT', 'FAILED', name='messagebatchstatuses', create_type=False), nullable=True),
sa.Column('status', postgresql.ENUM('not_sent', 'sent', 'failed', name='messagebatchstatuses', create_type=False), nullable=True),
sa.PrimaryKeyConstraint('batch_id')
)
# ### end Alembic commands ###
Expand All @@ -35,5 +35,5 @@ def upgrade() -> None:
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('message_batches')
sa.Enum('NOT_SENT', 'SENT', 'FAILED', name='messagebatchstatuses').drop(op.get_bind())
sa.Enum('not_sent', 'sent', 'failed', name='messagebatchstatuses').drop(op.get_bind())
# ### end Alembic commands ###
21 changes: 10 additions & 11 deletions database/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
from sqlalchemy import (
Column,
String,
Text,
TIMESTAMP,
)
from sqlalchemy import Column, ForeignKey, Integer, String, Text, TIMESTAMP
from sqlalchemy.dialects import postgresql
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.ext.declarative import declarative_base
import enum
import uuid

Base = declarative_base()

Expand Down Expand Up @@ -45,20 +41,23 @@ class MessageStatuses(enum.Enum):
class MessageBatch(Base):
__tablename__ = "message_batches"

batch_id = Column(UUID(as_uuid=True), nullable=False, primary_key=True)
id = Column(Integer, primary_key=True),
batch_id = Column(String, nullable=True)
batch_reference = Column(UUID(as_uuid=True), nullable=False)
created_at = Column(TIMESTAMP, nullable=False, default="NOW()")
details = Column(JSONB, nullable=True)
response = Column(JSONB, nullable=True)
status = Column(postgresql.ENUM(MessageBatchStatuses), default=MessageBatchStatuses.NOT_SENT)


class Message(Base):
__tablename__ = "messages"

batch_id = Column(UUID(as_uuid=True), nullable=False)
id = Column(Integer, primary_key=True),
batch_id = Column(Integer, ForeignKey("message_batches.id"), nullable=False),
created_at = Column(TIMESTAMP, nullable=False, default="NOW()")
details = Column(JSONB, nullable=True)
message_id = Column(String, nullable=False, primary_key=True)
message_id = Column(String, nullable=False)
message_reference = Column(UUID(as_uuid=True), nullable=False)
nhs_number = Column(Text, nullable=False)
recipient_id = Column(UUID(as_uuid=True), nullable=False)
Expand All @@ -70,7 +69,7 @@ class ChannelStatus(Base):
created_at = Column(TIMESTAMP, nullable=False, default="NOW()")
details = Column(JSONB, nullable=True)
idempotency_key = Column(Text, primary_key=True)
message_id = Column(String, default="UNKNOWN")
message_id = Column(String, nullable=True)
message_reference = Column(UUID(as_uuid=True), nullable=False)
status = Column(postgresql.ENUM(ChannelStatuses), nullable=True)

Expand All @@ -81,6 +80,6 @@ class MessageStatus(Base):
created_at = Column(TIMESTAMP, nullable=False, default="NOW()")
details = Column(JSONB, nullable=True)
idempotency_key = Column(Text, primary_key=True)
message_id = Column(String, default="UNKNOWN")
message_id = Column(String, nullable=True)
message_reference = Column(UUID(as_uuid=True), nullable=False)
status = Column(postgresql.ENUM(MessageStatuses), default=MessageStatuses.CREATED)
20 changes: 20 additions & 0 deletions src/notify/app/route_handlers/messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from flask import request
import app.validators.request_validator as request_validator


def batch():
json_data = request.json or {}
valid_headers, error_message = request_validator.verify_headers(dict(request.headers))

if not valid_headers:
return {"status": error_message}, 401

if not request_validator.verify_signature(dict(request.headers), json_data):
return {"status": "Invalid signature"}, 403

valid_body, error_message = request_validator.verify_body(json_data)

if not valid_body:
return {"status": error_message}, 422

return {"status": "success"}, 200
31 changes: 31 additions & 0 deletions src/notify/app/services/batch_message_notifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import app.utils.access_token as access_token
import logging
import os
import requests
import uuid


def notify(body: dict) -> dict:
response = requests.post(url(), json=body, headers=headers())

logging.info(f"Response from Notify API {url()}: {response.status_code}")

if response.status_code == 201:
logging.debug(response.text)
else:
logging.error(response.text)

return response.json()


def headers() -> dict:
return {
"content-type": "application/vnd.api+json",
"accept": "application/vnd.api+json",
"x-correlation-id": str(uuid.uuid4()),
"authorization": "Bearer " + access_token.get_token(),
}


def url() -> str:
return f"{os.getenv("NOTIFY_API_URL")}/comms/v1/message-batches"
26 changes: 26 additions & 0 deletions src/notify/app/services/message_batch_recorder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from sqlalchemy import engine, Session
import database.models as models


def save_message_batch(batch_reference: str, data: dict, response: dict, status: str) -> tuple[bool, str]:
batch = models.MessageBatch(
batch_id=response["data"]["id"],
batch_reference=batch_reference,
details=data,
response=response,
status=status,
)
with Session(engine) as session:
session.add(batch)
session.flush()


for message_data in data["attributes"]["messages"]:
models.Message(
batch_id=batch.id,
details=message_data,
message_id=message_data["id"],
message_reference=message_data["messageReference"],
nhs_number=message_data["nhs_number"],
recipient_id=message_data["recipient_id"],
)
20 changes: 13 additions & 7 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import app.utils.database as database
import dotenv
import logging
import os
import psycopg2
import pytest

if not bool(os.getenv("CI")):
dotenv.load_dotenv(".env.test")


# Inserts the human readable docstring as the nodeid for the test
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
Expand All @@ -19,18 +22,21 @@ def pytest_runtest_makereport(item, call):
report.nodeid = docstring

location = list(report.location)
location[0] = docstring
location[0] = f"{docstring} {location[0]}"
report.location = tuple(location)


@pytest.fixture(autouse=True, scope="function")
def truncate_table():
with database.connection() as conn:
with conn.cursor() as cur:
cur.execute("TRUNCATE TABLE batch_messages")
cur.execute("TRUNCATE TABLE channel_statuses")
cur.execute("TRUNCATE TABLE message_statuses")
cur.connection.commit()
try:
with database.connection() as conn:
with conn.cursor() as cur:
cur.execute("TRUNCATE TABLE batch_messages")
cur.execute("TRUNCATE TABLE channel_statuses")
cur.execute("TRUNCATE TABLE message_statuses")
cur.connection.commit()
except psycopg2.OperationalError as e:
logging.error(f"Error: {e}")


@pytest.fixture
Expand Down

0 comments on commit 442e062

Please sign in to comment.