Skip to content

DTOSS-10350: Record updates to Appointments from NBSS#248

Merged
Harriethw merged 6 commits intomainfrom
DTOSS-10350-record-cancellations
Aug 6, 2025
Merged

DTOSS-10350: Record updates to Appointments from NBSS#248
Harriethw merged 6 commits intomainfrom
DTOSS-10350-record-cancellations

Conversation

@Harriethw
Copy link
Copy Markdown
Contributor

@Harriethw Harriethw commented Aug 1, 2025

Description

NBSS sends data about an Appointment according to certain event triggers - Booked, Cancel, Update - and includes a status indicator on an Appointment where:

‘B’ = Booked
‘C’ = cancelled
‘A’ = Attended
‘D’ = DNA

We need the ability to update statuses of existing Appointments from messages we've already ingested based on a new update coming in from NBSS - e.g. if an Appointment goes from booked to cancelled.

The Appointments coming from NBSS into the MESH inbox will have an ID from NBSS to identify them, and we will use that to match with Appointments already created in our db.

This PR

  • Makes nbss_id a unique field to avoid any duplications
  • rather than getting or creating the Appointment object, we update or create one based on the nbss_id
  • add an nbss_updated_at field to capture the Action Timestamp from NBSS - in the documentation we have on the MESH inbox this field is described as Date/timestamp for triggering the data transfer.  we think data transfers run every 5 mins so this should be as close as we need to when the actual event happened?
    Not totally clear that we need this field in terms of user needs for reporting/auditing, as that is all presumable dealt with in NBSS, but it does seem right that as we are updating fields on an object we should record when that update happened.

Jira link

https://nhsd-jira.digital.nhs.uk/browse/DTOSS-10350

Review notes

@Harriethw Harriethw force-pushed the DTOSS-10350-record-cancellations branch from 6a8d68c to 01ecb79 Compare August 1, 2025 11:42
@Harriethw Harriethw requested a review from a team August 1, 2025 14:02
@Harriethw Harriethw marked this pull request as ready for review August 1, 2025 14:02
return Appointment.objects.update_or_create(
nbss_id=row["Appointment ID"],
defaults={
"nhs_number": row["NHS Num"],
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here I'm potentially updating any field on the Appointment that isn't nbss_id - is that ok? Should we take whatever the latest data is on an Appointment, as presumable should match NBSS? or should we only update e.g. status so that we keep a snapshot of the original content that we stored?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case of a cancellation I think we should only update the fields relating to the cancellation - the status, cancelled_by and the related timestamp coming from the Action Timestamp column.
Probably out of scope for this PR but is there a chance that a Booked Appointment can be overwritten with new details? Probably something for the team to investigate and outside the scope of what we're addressing here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case of a cancellation I think we should only update the fields relating to the cancellation - the status, cancelled_by and the related timestamp coming from the Action Timestamp column.

This does make sense, I guess better to be as specific as possible - but I'm getting a bit into the weeds thinking of all the possible complications around how to decide whether to create/update and what to create/update. E.g. what if the original Booked appointment had one of it's fields different to the Cancelled appointment coming in? Can we rely on all the data being exactly the same? If that were the case we can't match on all the fields, only the nbss_id so we need to check if it exists first and then do something slightly different for all the statuses like below.
I'm also imagining that we can't assume if a Cancellation is coming in that there will be a Booked one already there, especially when we first spin up.
I don't know if I'm getting way too complicated with this 😅

        status = row["Status"]
        nbss_id = row["Appointment ID"]
        appointment = Appointment.objects.filter(nbss_id=nbss_id).first()

        if status == "C":
            if appointment != None:
                appointment.status = status
                appointment.cancelled_by = row["Cancelled By"]
                appointment.save()
                return (appointment, False)
            else:
              return (Appointment.objects.create(
                  nbss_id=row["Appointment ID"],
                  nhs_number=row["NHS Num"],
                  clinic=clinic,
                  starts_at=self.appointment_date_and_time(row),
                  number=row["Sequence"],
                  status=status,
                  booked_by=row["Booked By"],
                  cancelled_by=row["Cancelled By"]
              ), True)
        elif status == "B":
            if appointment != None:
                appointment.status = status
                appointment.booked_by = row["Booked By"]
                appointment.save()
                return (appointment, False)
            else:
                return (Appointment.objects.create(
                    nbss_id=row["Appointment ID"],
                    nhs_number=row["NHS Num"],
                    clinic=clinic,
                    starts_at=self.appointment_date_and_time(row),
                    number=row["Sequence"],
                    status=status,
                    booked_by=row["Booked By"],
                ), True)
        else:
            if appointment != None:
                appointment.status = status
                appointment.save()
                return (appointment, False)
            else:
                return (Appointment.objects.create(
                    nbss_id=row["Appointment ID"],
                    nhs_number=row["NHS Num"],
                    clinic=clinic,
                    starts_at=self.appointment_date_and_time(row),
                    number=row["Sequence"],
                    status=row["Status"],
                    booked_by=row["Booked By"],
                    cancelled_by=row["Cancelled By"]
                ), True)

Copy link
Copy Markdown
Contributor Author

@Harriethw Harriethw Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about the above... I think we can safely assume the nhs_number, starts_at will be the same, but clinic might have slightly different address fields? So we could condense to something like

        defaults = {
            "number": row["Sequence"],
            "status": row["Status"],
            "clinic": row["Clinic"],
            # Some logic to work out whether to updated cancelled or booked based on status
            "cancelled_by": row["Cancelled By"],
            "booked_by": row["Booked By"],
            "cancelled_at": row["Action Timestamp"]
        }

        return Appointment.objects.update_or_create(
            nbss_id=row["Appointment ID"],
            nhs_number=row["NHS Num"],
            starts_at=self.appointment_date_and_time(row),
            defaults=defaults
        )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that nbss_id, nhs_number and starts_at are good query params for the update_or_create statement.
I think clinic is found/created by clinic code via a separate query, and in any case we probably aren't interested in whether clinic has changed in the case of a cancellation (I would err on the side of preserving the original value).
There is definitely a set of scenarios for updates to existing bookings we need to cover but for this PR let's just make sure status, cancelled_by and cancelled_at (or whatever timestamp we use) are updated here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok I've just focused on the cancel use case and tried to keep as simple as possible. a few gotchas the tests uncovered - the number field is from the sequence row which apparently is just the row number of the dat file? so it will change based on where the appointment is in the data. I'm not sure what the implications of that are...
I had to still account for the booked scenario in order to cover existing behaviour of updating booked_by.

Copy link
Copy Markdown
Contributor

@steventux steventux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, I've commented about what I think we should update.
I've also made a comment about the nbss_updated_at timestamp - happy to chat about this.

operations = [
migrations.AddField(
model_name='appointment',
name='nbss_updated_at',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard to know what we'll actually need for reporting but I'm not sure this date field gives us the precision on when things change because of a cancellation.
Might just be my Rails/ActiveRecord bias but created_at is the timestamp NOW() from postgres - so transaction commit time.
nbss_updated_at is a past event that occurred in NBSS, perhaps less than a minute ago but not an event in Manage.
If we create a "booked" appointment and then it is cancelled we lose the information about the nbss booking time because nbss_updated_at is overwritten.
Comparing created_at to nbss_updated_at is not a great indicator of when something was booked vs. cancelled in this scenario because the timestamps refer to events on different systems.
Although a bit more involved, I wonder if we would preserve more accuracy if we had:
created_at and updated_at as Manage db timestamps and booked_at, cancelled_at as NBSS timestamps (coming from the Action Timestamp column of the dat file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created_at and updated_at as Manage db timestamps and booked_at, cancelled_at as NBSS timestamps (coming from the Action Timestamp column of the dat file.

yep that makes a lot more sense!

return Appointment.objects.update_or_create(
nbss_id=row["Appointment ID"],
defaults={
"nhs_number": row["NHS Num"],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case of a cancellation I think we should only update the fields relating to the cancellation - the status, cancelled_by and the related timestamp coming from the Action Timestamp column.
Probably out of scope for this PR but is there a chance that a Booked Appointment can be overwritten with new details? Probably something for the team to investigate and outside the scope of what we're addressing here.


id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
nbss_id = models.CharField(max_length=30)
nbss_id = models.CharField(max_length=30, unique=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

because we already have recommended settings for
vscode, these settings need to be part of the file
in order for pytest to work in the UI. Alternative
is to be less opinionated and gitignore this file.
@Harriethw Harriethw force-pushed the DTOSS-10350-record-cancellations branch from 01ecb79 to fe50348 Compare August 5, 2025 14:10
we will shortly be changing how this is assigned
when updating Appointments.
We need the ability to update statuses of
Appointments based on events coming in from NBSS -
e.g. if an Appointment is cancelled. The
Appointments coming from NBSS into the MESH inbox
will have an ID from NBSS to identify them, and we
will use that to match with Appointments already
created in our db, so it should be a unique id.
This updates an Appointment if one already exists
with the Appointment ID from NBSS

This is assuming that the NBSS ID and every field
except status and cancelled/booked_by is
consistent.

Will next be adding datetime stamps to show when
the model cancelled or booked, based on the data we
have from NBSS.
this will allow us to add the Action Timestamp from the NBSS
data to the `cancelled_at` field, and adding an autoupdating
`updated_at` field as good practice to show when our db
last updated the object.
@Harriethw Harriethw force-pushed the DTOSS-10350-record-cancellations branch from fe50348 to 8f660b4 Compare August 5, 2025 14:18
Copy link
Copy Markdown
Contributor

@steventux steventux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💾

@Harriethw Harriethw merged commit 8d6163b into main Aug 6, 2025
7 checks passed
@Harriethw Harriethw deleted the DTOSS-10350-record-cancellations branch August 6, 2025 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants