Repository: beyondessential/tamanu
Pull request #7475: feat: EPI-1128: Build workflow to record a medication as Given
Author: login: huytranbes
Created: 2025-03-27T18:20:49Z | Merged: 2025-04-17T16:04:53Z

### Changes

_Add a brief description of the changes in this PR to help give the reviewer context._

### Deploys

- [ ] **Deploy to Tamanu Internal** <!-- #deploy -->

### Tests

- [ ] **Run E2E Tests** <!-- #e2e -->

### Remember to...

- ...write or update tests
- ...add UI screenshots and **testing notes** to the Linear issue
- ...add any **manual upgrade steps** to the Linear issue
- ...update the [config reference](https://beyond-essential.slab.com/posts/reference-config-file-0c70ukly), [settings reference](https://beyond-essential.slab.com/posts/reference-settings-0blw1x2q), or any [relevant runbook(s)](https://beyond-essential.slab.com/topics/runbooks-bs04ml6c)
- ...call out additions or changes to **config files** for the deployment team to take note of

<!-- Thank you! -->



Diff:
diff --git a/database/model/public/medication_administration_record_doses.md b/database/model/public/medication_administration_record_doses.md
new file mode 100644
index 00000000000..c5f0feceb29
--- /dev/null
+++ b/database/model/public/medication_administration_record_doses.md
@@ -0,0 +1,23 @@
+{% docs table__medication_administration_record_doses %}
+Stores details about individual doses given as part of a medication administration record. A single administration event may involve multiple distinct doses (e.g., taking two tablets).
+{% enddocs %}
+
+{% docs medication_administration_record_doses__dose_amount %}
+The amount or quantity of the medication administered in this specific dose entry.
+{% enddocs %}
+
+{% docs medication_administration_record_doses__given_time %}
+The precise date and time when this specific dose was administered to the patient. This may differ slightly from the overall `administered_at` time in the parent record if multiple doses were given sequentially.
+{% enddocs %}
+
+{% docs medication_administration_record_doses__given_by_user_id %}
+Reference to the [user](#!/model/model.public.users) who physically administered this dose to the patient.
+{% enddocs %}
+
+{% docs medication_administration_record_doses__recorded_by_user_id %}
+Reference to the [user](#!/model/model.public.users) who recorded this dose administration in the system. This may or may not be the same user who administered the dose.
+{% enddocs %}
+
+{% docs medication_administration_record_doses__mar_id %}
+Reference to the MAR [medication administration record](#!/model/model.public.medication_administration_records) to which this dose belongs.
+{% enddocs %}
diff --git a/database/model/public/medication_administration_record_doses.yml b/database/model/public/medication_administration_record_doses.yml
new file mode 100644
index 00000000000..dff33c9e7e5
--- /dev/null
+++ b/database/model/public/medication_administration_record_doses.yml
@@ -0,0 +1,63 @@
+version: 2
+sources:
+  - name: tamanu
+    schema: public
+    description: "{{ doc('generic__schema') }}"
+    tables:
+      - name: medication_administration_record_doses
+        description: '{{ doc("table__medication_administration_record_doses") }}'
+        tags: []
+        columns:
+          - name: id
+            data_type: uuid
+            description: "{{ doc('generic__id') }} in
+              medication_administration_record_doses."
+            data_tests:
+              - unique
+              - not_null
+          - name: dose_amount
+            data_type: numeric
+            description: "{{ doc('medication_administration_record_doses__dose_amount') }}"
+            data_tests:
+              - not_null
+          - name: given_time
+            data_type: character(19)
+            description: "{{ doc('medication_administration_record_doses__given_time') }}"
+            data_tests:
+              - not_null
+          - name: given_by_user_id
+            data_type: character varying(255)
+            description: "{{ doc('medication_administration_record_doses__given_by_user_id')
+              }}"
+          - name: recorded_by_user_id
+            data_type: character varying(255)
+            description: "{{
+              doc('medication_administration_record_doses__recorded_by_user_id')
+              }}"
+          - name: mar_id
+            data_type: character varying(255)
+            description: "{{ doc('medication_administration_record_doses__mar_id') }}"
+            data_tests:
+              - not_null
+          - name: created_at
+            data_type: timestamp with time zone
+            description: "{{ doc('generic__created_at') }} in
+              medication_administration_record_doses."
+            data_tests:
+              - not_null
+          - name: updated_at
+            data_type: timestamp with time zone
+            description: "{{ doc('generic__updated_at') }} in
+              medication_administration_record_doses."
+            data_tests:
+              - not_null
+          - name: deleted_at
+            data_type: timestamp with time zone
+            description: "{{ doc('generic__deleted_at') }} in
+              medication_administration_record_doses."
+          - name: updated_at_sync_tick
+            data_type: bigint
+            description: "{{ doc('generic__updated_at_sync_tick') }} in
+              medication_administration_record_doses."
+            data_tests:
+              - not_null
diff --git a/packages/constants/src/enumRegistry.ts b/packages/constants/src/enumRegistry.ts
index 9a33cfef309..9679a09f27f 100644
--- a/packages/constants/src/enumRegistry.ts
+++ b/packages/constants/src/enumRegistry.ts
@@ -40,7 +40,12 @@ import { TEMPLATE_TYPE_LABELS } from './templates.js';
 import { LAB_REQUEST_STATUS_LABELS } from './labs.js';
 import { ASSET_NAME_LABELS } from './importable.js';
 import { DIAGNOSIS_CERTAINTY_LABELS, PATIENT_ISSUE_LABELS } from './diagnoses.js';
-import { DRUG_ROUTE_LABELS, DRUG_UNIT_LABELS, REPEATS_LABELS } from './medications.js';
+import {
+  DRUG_ROUTE_LABELS,
+  DRUG_UNIT_LABELS,
+  DRUG_UNIT_SHORT_LABELS,
+  REPEATS_LABELS,
+} from './medications.js';
 import { PLACE_OF_DEATHS, MANNER_OF_DEATHS } from './deaths.js';
 import { LOCATION_AVAILABILITY_STATUS_LABELS } from './locations.js';
 import { TASK_FREQUENCY_UNIT_LABELS, TASK_DURATION_UNIT_LABELS } from './tasks.js';
@@ -71,6 +76,7 @@ export const registeredEnums = {
   DIAGNOSIS_CERTAINTY_LABELS,
   DRUG_ROUTE_LABELS,
   DRUG_UNIT_LABELS,
+  DRUG_UNIT_SHORT_LABELS,
   EDUCATIONAL_ATTAINMENT_LABELS,
   ENCOUNTER_TYPE_LABELS,
   IMAGING_TYPES,
@@ -124,6 +130,7 @@ export const translationPrefixes: Record<EnumKeys, string> = {
   DIAGNOSIS_CERTAINTY_LABELS: 'diagnosis.property.certainty',
   DRUG_ROUTE_LABELS: 'medication.property.route',
   DRUG_UNIT_LABELS: 'medication.property.unit',
+  DRUG_UNIT_SHORT_LABELS: 'medication.property.unitShort',
   EDUCATIONAL_ATTAINMENT_LABELS: 'patient.property.educationalAttainment',
   ENCOUNTER_TYPE_LABELS: 'encounter.property.type',
   IMAGING_TYPES: 'imaging.property.type',
diff --git a/packages/database/src/migrations/1742897789276-addMedicationAdministrationRecordDoseTable.ts b/packages/database/src/migrations/1742897789276-addMedicationAdministrationRecordDoseTable.ts
new file mode 100644
index 00000000000..dbab41ed7e4
--- /dev/null
+++ b/packages/database/src/migrations/1742897789276-addMedicationAdministrationRecordDoseTable.ts
@@ -0,0 +1,50 @@
+import { DataTypes, QueryInterface, Sequelize } from 'sequelize';
+
+export async function up(query: QueryInterface): Promise<void> {
+  await query.createTable('medication_administration_record_doses', {
+    id: {
+      type: DataTypes.UUID,
+      allowNull: false,
+      primaryKey: true,
+      defaultValue: Sequelize.fn('gen_random_uuid'),
+    },
+    dose_amount: {
+      type: DataTypes.DECIMAL,
+      allowNull: false,
+    },
+    given_time: {
+      type: DataTypes.DATETIMESTRING,
+      allowNull: false,
+    },
+    given_by_user_id: {
+      type: DataTypes.STRING,
+      allowNull: true,
+    },
+    recorded_by_user_id: {
+      type: DataTypes.STRING,
+      allowNull: true,
+    },
+    mar_id: {
+      type: DataTypes.STRING,
+      allowNull: false,
+    },
+    created_at: {
+      type: DataTypes.DATE,
+      allowNull: false,
+      defaultValue: Sequelize.fn('now'),
+    },
+    updated_at: {
+      type: DataTypes.DATE,
+      allowNull: false,
+      defaultValue: Sequelize.fn('now'),
+    },
+    deleted_at: {
+      type: DataTypes.DATE,
+      allowNull: true,
+    },
+  });
+}
+
+export async function down(query: QueryInterface): Promise<void> {
+  await query.dropTable('medication_administration_record_doses');
+}
diff --git a/packages/database/src/models/MedicationAdministrationRecord.ts b/packages/database/src/models/MedicationAdministrationRecord.ts
index 5bd6d223110..2d355bd6e8f 100644
--- a/packages/database/src/models/MedicationAdministrationRecord.ts
+++ b/packages/database/src/models/MedicationAdministrationRecord.ts
@@ -193,6 +193,10 @@ export class MedicationAdministrationRecord extends Model {
       foreignKey: 'reasonNotGivenId',
       as: 'reasonNotGiven',
     });
+    this.hasMany(models.MedicationAdministrationRecordDose, {
+      foreignKey: 'marId',
+      as: 'doses',
+    });
   }
 
   static buildSyncFilter() {
diff --git a/packages/database/src/models/MedicationAdministrationRecordDose.ts b/packages/database/src/models/MedicationAdministrationRecordDose.ts
new file mode 100644
index 00000000000..7477d65112c
--- /dev/null
+++ b/packages/database/src/models/MedicationAdministrationRecordDose.ts
@@ -0,0 +1,67 @@
+import { DataTypes } from 'sequelize';
+import { SYNC_DIRECTIONS } from '@tamanu/constants';
+import { Model } from './Model';
+import { dateTimeType, type InitOptions, type Models } from '../types/model';
+
+export class MedicationAdministrationRecordDose extends Model {
+  declare id: string;
+  declare doseAmount: number;
+  declare givenTime: Date;
+  declare givenByUserId?: string;
+  declare recordedByUserId?: string;
+  declare marId: string;
+
+  static initModel({ primaryKey, ...options }: InitOptions) {
+    super.init(
+      {
+        id: primaryKey,
+        doseAmount: {
+          type: DataTypes.DECIMAL,
+          allowNull: false,
+        },
+        givenTime: dateTimeType('givenTime', {
+          allowNull: false,
+        }),
+        givenByUserId: {
+          type: DataTypes.STRING,
+        },
+        recordedByUserId: {
+          type: DataTypes.STRING,
+        },
+        marId: {
+          type: DataTypes.STRING,
+          allowNull: false,
+        },
+      },
+      {
+        ...options,
+        syncDirection: SYNC_DIRECTIONS.BIDIRECTIONAL,
+      },
+    );
+  }
+
+  static initRelations(models: Models) {
+    this.belongsTo(models.MedicationAdministrationRecord, {
+      foreignKey: 'marId',
+      as: 'medicationAdministrationRecord',
+    });
+    
+    this.belongsTo(models.User, {
+      foreignKey: 'givenByUserId',
+      as: 'givenByUser',
+    });
+    
+    this.belongsTo(models.User, {
+      foreignKey: 'recordedByUserId',
+      as: 'recordedByUser',
+    });
+  }
+
+  static buildSyncFilter() {
+    return null; // syncs everywhere
+  }
+
+  static buildSyncLookupQueryDetails() {
+    return null; // syncs everywhere
+  }
+} 
diff --git a/packages/database/src/models/index.ts b/packages/database/src/models/index.ts
index 36349122556..80c311047d6 100644
--- a/packages/database/src/models/index.ts
+++ b/packages/database/src/models/index.ts
@@ -138,3 +138,4 @@ export * from './SyncDeviceTick';
 
 export * from './ReferenceDrug';
 export * from './MedicationAdministrationRecord';
+export * from './MedicationAdministrationRecordDose';
diff --git a/packages/facility-server/app/routes/apiv1/encounter.js b/packages/facility-server/app/routes/apiv1/encounter.js
index f6b5a1fff29..e7de666e874 100644
--- a/packages/facility-server/app/routes/apiv1/encounter.js
+++ b/packages/facility-server/app/routes/apiv1/encounter.js
@@ -203,7 +203,13 @@ encounterRelations.get(
         {
           model: models.MedicationAdministrationRecord,
           as: 'medicationAdministrationRecords',
-          include: 'reasonNotGiven'
+          include: [
+            'reasonNotGiven', 
+            {
+              association: 'doses',
+              order: [['givenTime', 'ASC']]
+            }
+          ],
         },
         {
           model: models.EncounterPrescription,
diff --git a/packages/facility-server/app/routes/apiv1/medication.js b/packages/facility-server/app/routes/apiv1/medication.js
index 34423236cb9..8c7edeb103c 100644
--- a/packages/facility-server/app/routes/apiv1/medication.js
+++ b/packages/facility-server/app/routes/apiv1/medication.js
@@ -458,14 +458,99 @@ medication.get(
   }),
 );
 
-const notGivenInputUpdateSchema = z
-  .object({
-    reasonNotGivenId: z.string(),
-  })
-  .strip();
+const givenMarUpdateSchema = z.object({
+  dose: z.object({
+    doseAmount: z.number(),
+    givenTime: z.string().datetime(),
+  }),
+});
+medication.put(
+  '/medication-administration-record/:id/given',
+  asyncHandler(async (req, res) => {
+    req.checkPermission('write', 'MedicationAdministrationRecord');
+    const { models, params } = req;
+    const { MedicationAdministrationRecord, MedicationAdministrationRecordDose } = models;
+
+    const { dose } = await givenMarUpdateSchema.parseAsync(req.body);
+
+    const record = await MedicationAdministrationRecord.findByPk(params.id);
+    if (!record) {
+      throw new InvalidOperationError(`MAR with id ${params.id} not found`);
+    }
+
+    if (record.status === ADMINISTRATION_STATUS.GIVEN) {
+      throw new InvalidOperationError(`MAR with id ${params.id} is already given`);
+    }
+
+    //Update MAR and add dose to the MAR
+    record.status = ADMINISTRATION_STATUS.GIVEN;
+    if (!record.recordedAt) {
+      record.recordedAt = getCurrentDateTimeString();
+    }
+    await record.save();
+    await MedicationAdministrationRecordDose.create({
+      marId: record.id,
+      doseAmount: dose.doseAmount,
+      givenTime: dose.givenTime,
+    });
+
+    res.send(record.forResponse());
+  }),
+);
+
+const givenMarCreateSchema = z.object({
+  dose: z.object({
+    doseAmount: z.number(),
+    givenTime: z.string().datetime(),
+  }),
+  dueAt: z.string().datetime(),
+  prescriptionId: z.string(),
+});
+medication.post(
+  '/medication-administration-record/given',
+  asyncHandler(async (req, res) => {
+    const { models } = req;
+    const { MedicationAdministrationRecord, MedicationAdministrationRecordDose, Prescription } =
+      models;
 
+    req.checkPermission('create', 'MedicationAdministrationRecord');
+    const { dose, dueAt, prescriptionId } = await givenMarCreateSchema.parseAsync(req.body);
+
+    //validate prescription
+    const prescription = await Prescription.findByPk(prescriptionId);
+    if (!prescription) {
+      throw new InvalidOperationError(`Prescription with id ${prescriptionId} not found`);
+    }
+
+    //validate dose
+    if (dose.doseAmount <= 0) {
+      throw new InvalidOperationError(`Dose amount must be greater than 0`);
+    }
+
+    //create MAR
+    const record = await MedicationAdministrationRecord.create({
+      dueAt,
+      prescriptionId,
+      status: ADMINISTRATION_STATUS.GIVEN,
+      recordedAt: getCurrentDateTimeString(),
+    });
+
+    //create dose
+    await MedicationAdministrationRecordDose.create({
+      marId: record.id,
+      doseAmount: dose.doseAmount,
+      givenTime: dose.givenTime,
+    });
+
+    res.send(record.forResponse());
+  }),
+);
+
+const notGivenInputUpdateSchema = z.object({
+  reasonNotGivenId: z.string(),
+});
 medication.put(
-  '/mar/:id/notGiven',
+  '/medication-administration-record/:id/not-given',
   asyncHandler(async (req, res) => {
     req.checkPermission('write', 'MedicationAdministrationRecord');
     const { models, params } = req;
@@ -481,36 +566,34 @@ medication.put(
       throw new InvalidOperationError(`Not given reason with id ${reasonNotGivenId} not found`);
     }
 
-    const mar = await MedicationAdministrationRecord.findByPk(params.id);
-    if (!mar) {
+    const record = await MedicationAdministrationRecord.findByPk(params.id);
+    if (!record) {
       throw new InvalidOperationError(`MAR with id ${params.id} not found`);
     }
 
-    if (mar.status === ADMINISTRATION_STATUS.NOT_GIVEN) {
+    if (record.status === ADMINISTRATION_STATUS.NOT_GIVEN) {
       throw new InvalidOperationError(`MAR with id ${params.id} is already not given`);
     }
 
     //Update MAR
-    mar.reasonNotGivenId = reasonNotGivenId;
-    mar.status = ADMINISTRATION_STATUS.NOT_GIVEN;
-    if (!mar.status) {
-      mar.recordedAt = getCurrentDateTimeString();
+    record.reasonNotGivenId = reasonNotGivenId;
+    record.status = ADMINISTRATION_STATUS.NOT_GIVEN;
+    if (!record.recordedAt) {
+      record.recordedAt = getCurrentDateTimeString();
     }
-    await mar.save();
+    await record.save();
 
-    res.send(mar.forResponse());
+    res.send(record.forResponse());
   }),
 );
 
-const notGivenInputCreateSchema = z
-  .object({
-    reasonNotGivenId: z.string(),
-    dueAt: z.string().datetime(),
-    prescriptionId: z.string(),
-  })
-  .strip();
+const notGivenInputCreateSchema = z.object({
+  reasonNotGivenId: z.string(),
+  dueAt: z.string().datetime(),
+  prescriptionId: z.string(),
+});
 medication.post(
-  '/mar/notGiven',
+  '/medication-administration-record/not-given',
   asyncHandler(async (req, res) => {
     req.checkPermission('create', 'MedicationAdministrationRecord');
     const { models } = req;
@@ -535,7 +618,7 @@ medication.post(
     }
 
     //create MAR
-    const mar = await MedicationAdministrationRecord.create({
+    const record = await MedicationAdministrationRecord.create({
       reasonNotGivenId,
       dueAt,
       prescriptionId,
@@ -543,7 +626,7 @@ medication.post(
       recordedAt: getCurrentDateTimeString(),
     });
 
-    res.send(mar.forResponse());
+    res.send(record.forResponse());
   }),
 );
 
diff --git a/packages/web/app/api/mutations/useMarMutation.js b/packages/web/app/api/mutations/useMarMutation.js
index 5460fc590da..3340e57c694 100644
--- a/packages/web/app/api/mutations/useMarMutation.js
+++ b/packages/web/app/api/mutations/useMarMutation.js
@@ -7,8 +7,25 @@ export const useNotGivenMarMutation = (existingMarId = null, useMutationOptions
   return useMutation(
     payload =>
       existingMarId
-        ? api.put(`medication/mar/${existingMarId}/notGiven`, payload)
-        : api.post('medication/mar/notGiven', payload),
+        ? api.put(`medication/medication-administration-record/${existingMarId}/not-given`, payload)
+        : api.post('medication/medication-administration-record/not-given', payload),
+    {
+      ...useMutationOptions,
+      onSuccess: (data, variables, context) => {
+        useMutationOptions.onSuccess?.(data, variables, context);
+      },
+    },
+  );
+};
+
+export const useGivenMarMutation = (existingMarId = null, useMutationOptions = {}) => {
+  const api = useApi();
+
+  return useMutation(
+    payload =>
+      existingMarId
+        ? api.put(`medication/medication-administration-record/${existingMarId}/given`, payload)
+        : api.post('medication/medication-administration-record/given', payload),
     {
       ...useMutationOptions,
       onSuccess: (data, variables, context) => {
diff --git a/packages/web/app/components/Field/TimePickerField.jsx b/packages/web/app/components/Field/TimePickerField.jsx
new file mode 100644
index 00000000000..c1d16c7ae42
--- /dev/null
+++ b/packages/web/app/components/Field/TimePickerField.jsx
@@ -0,0 +1,6 @@
+import React from 'react';
+import { TimePicker } from '@mui/x-date-pickers/TimePicker';
+
+export const TimePickerField = ({ field, ...props }) => (
+  <TimePicker name={field.name} value={field.value} onChange={field.onChange} {...props} />
+);
diff --git a/packages/web/app/components/Medication/MarStatus.jsx b/packages/web/app/components/Medication/MarStatus.jsx
index 9a45198c60d..d8ab6037d1e 100644
--- a/packages/web/app/components/Medication/MarStatus.jsx
+++ b/packages/web/app/components/Medication/MarStatus.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState } from 'react';
+import React, { useRef, useState } from 'react';
 import styled from 'styled-components';
 import { Box } from '@material-ui/core';
 import CheckCircleIcon from '@material-ui/icons/CheckCircle';
@@ -6,12 +6,15 @@ import { addHours, format } from 'date-fns';
 import CancelIcon from '@material-ui/icons/Cancel';
 import HelpOutlineIcon from '@material-ui/icons/HelpOutline';
 import PriorityHighIcon from '@material-ui/icons/PriorityHigh';
-import { ADMINISTRATION_STATUS, MEDICATION_ADMINISTRATION_TIME_SLOTS } from '@tamanu/constants';
+import {
+  ADMINISTRATION_STATUS,
+  DRUG_UNIT_SHORT_LABELS,
+  MEDICATION_ADMINISTRATION_TIME_SLOTS,
+} from '@tamanu/constants';
 import { getDateFromTimeString } from '@tamanu/shared/utils/medication';
 import { Colors } from '../../constants';
-import { TranslatedText } from '../Translation';
+import { TranslatedEnum, TranslatedText } from '../Translation';
 import { ConditionalTooltip } from '../Tooltip';
-import { getDose } from '../../utils/medications';
 import { useTranslation } from '../../contexts/Translation';
 import { StatusPopper } from './StatusPopper';
 import { WarningModal } from './WarningModal';
@@ -176,18 +179,11 @@ export const MarStatus = ({
   medication,
   pauseRecords,
 }) => {
-  const { dueAt, status, reasonNotGiven, id: marId } = marInfo || {};
-  const {
-    doseAmount,
-    isPrn,
-    units,
-    discontinuedDate,
-    endDate,
-    id: prescriptionId,
-    isVariableDose,
-  } = medication || {};
+  const { dueAt, status, reasonNotGiven, doses } = marInfo || {};
+  const { doseAmount, isPrn, units, discontinuedDate, endDate, isVariableDose } = medication || {};
 
   const [isSelected, setIsSelected] = useState(false);
+
   const [anchorEl, setAnchorEl] = useState(null);
   const [showWarningModal, setShowWarningModal] = useState('');
   const [selectedElement, setSelectedElement] = useState(null);
@@ -195,6 +191,7 @@ export const MarStatus = ({
   const containerRef = useRef(null);
   const isPast = getIsPast({ timeSlot, selectedDate });
   const isDisabled = getIsDisabled({ hasRecord: !!marInfo, timeSlot, selectedDate });
+  const isFuture = getDateFromTimeString(timeSlot.startTime, selectedDate) > new Date();
   const isDiscontinued = getIsDiscontinued({
     discontinuedDate,
     dueAt,
@@ -232,21 +229,7 @@ export const MarStatus = ({
     discontinuedDate,
   });
 
-  const { getTranslation, getEnumTranslation } = useTranslation();
-
-  useEffect(() => {
-    const handleClickOutside = event => {
-      if (containerRef.current && !containerRef.current.contains(event.target)) {
-        setIsSelected(false);
-      }
-    };
-
-    document.addEventListener('mousedown', handleClickOutside);
-
-    return () => {
-      document.removeEventListener('mousedown', handleClickOutside);
-    };
-  }, []);
+  const { getEnumTranslation } = useTranslation();
 
   const onSelected = event => {
     if (isDiscontinued || isDisabled) return;
@@ -263,7 +246,7 @@ export const MarStatus = ({
       setShowWarningModal(MAR_WARNING_MODAL.PAST);
       return;
     }
-    if (isDisabled) {
+    if (isFuture) {
       setShowWarningModal(MAR_WARNING_MODAL.FUTURE);
       return;
     }
@@ -328,7 +311,14 @@ export const MarStatus = ({
         }
         return (
           <DoseInfo>
-            {getDose({ doseAmount, units, isPrn }, getTranslation, getEnumTranslation)}
+            <div>
+              {isVariableDose ? (
+                <TranslatedText stringId="medication.table.variable" fallback="Variable" />
+              ) : (
+                doseAmount
+              )}
+            </div>
+            <div>{units ? getEnumTranslation(DRUG_UNIT_SHORT_LABELS, units) : ''}</div>
           </DoseInfo>
         );
       }
@@ -363,6 +353,23 @@ export const MarStatus = ({
               <div>{reasonNotGiven?.name}</div>
             </>
           );
+        case ADMINISTRATION_STATUS.GIVEN: {
+          const firstDose = doses?.[0];
+          if (!firstDose) return null;
+          return (
+            <Box maxWidth={73}>
+              <span>{firstDose?.doseAmount}</span>{' '}
+              <TranslatedEnum enumValues={DRUG_UNIT_SHORT_LABELS} value={units} />{' '}
+              <TranslatedText
+                stringId="medication.mar.givenAt.tooltip"
+                fallback="given at :time"
+                replacements={{
+                  time: format(new Date(firstDose?.givenTime), 'hh:mma').toLowerCase(),
+                }}
+              />
+            </Box>
+          );
+        }
         default:
           if (isDisabled) {
             return (
@@ -421,6 +428,17 @@ export const MarStatus = ({
       <ConditionalTooltip
         visible={getTooltipText()}
         title={<Box fontWeight={400}>{getTooltipText()}</Box>}
+        PopperProps={{
+          popperOptions: {
+            positionFixed: true,
+            modifiers: {
+              preventOverflow: {
+                enabled: true,
+                boundariesElement: 'window',
+              },
+            },
+          },
+        }}
       >
         <StatusContainer
           ref={containerRef}
@@ -439,9 +457,12 @@ export const MarStatus = ({
         open={Boolean(anchorEl)}
         anchorEl={anchorEl}
         onClose={handleClose}
-        marId={marId}
-        dueAt={dueAt ? dueAt : addHours(getDateFromTimeString(timeSlot.startTime, selectedDate), 1)}
-        prescriptionId={prescriptionId}
+        timeSlot={timeSlot}
+        selectedDate={selectedDate}
+        marInfo={marInfo}
+        medication={medication}
+        isFuture={isFuture}
+        isPast={isPast}
       />
       <WarningModal
         modal={showWarningModal}
diff --git a/packages/web/app/components/Medication/StatusPopper.jsx b/packages/web/app/components/Medication/StatusPopper.jsx
index 019284b8340..b8446636e27 100644
--- a/packages/web/app/components/Medication/StatusPopper.jsx
+++ b/packages/web/app/components/Medication/StatusPopper.jsx
@@ -1,14 +1,23 @@
-import React, { useState } from 'react';
+import React, { useLayoutEffect, useRef, useState } from 'react';
 import styled from 'styled-components';
 import { useQueryClient } from '@tanstack/react-query';
-import { Divider, Popper, Paper, ClickAwayListener, Fade } from '@material-ui/core';
+import { Divider, Popper, Paper, ClickAwayListener, Fade, IconButton } from '@material-ui/core';
+import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
+import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline';
+import { getDateFromTimeString } from '@tamanu/shared/utils/medication';
+import { addHours, set } from 'date-fns';
+import * as yup from 'yup';
 import { Colors } from '../../constants';
 import { TranslatedText } from '../Translation';
 import { Button } from '../Button';
 import { ADMINISTRATION_STATUS } from '@tamanu/constants';
-import { useNotGivenMarMutation } from '../../api/mutations/useMarMutation';
+import { useGivenMarMutation, useNotGivenMarMutation } from '../../api/mutations/useMarMutation';
 import { useEncounter } from '../../contexts/Encounter';
 import { useSuggestionsQuery } from '../../api/queries/useSuggestionsQuery';
+import { Field, Form, NumberField } from '../Field';
+import { TimePickerField } from '../Field/TimePickerField';
+import { MAR_WARNING_MODAL } from '../../constants/medication';
+import { WarningModal } from './WarningModal';
 
 const StyledPaper = styled(Paper)`
   box-shadow: 0px 8px 32px 0px #00000026;
@@ -57,21 +66,376 @@ const StyledButton = styled(Button)`
   border-color: ${p => p.$color} !important;
 `;
 
-export const StatusPopper = ({ marId, open, anchorEl, onClose, dueAt, prescriptionId }) => {
+const DoseContainer = styled.div`
+  display: flex;
+  align-items: center;
+  margin-bottom: 7px;
+  gap: 5px;
+`;
+
+const DoseButton = styled(IconButton)`
+  padding: 5px;
+  margin: -5px;
+  svg {
+    color: ${Colors.primary};
+    width: 14px;
+    height: 14px;
+  }
+`;
+
+const TimeGivenTitle = styled.div`
+  color: ${Colors.darkText};
+  font-size: 12px;
+  font-weight: 500;
+  margin-bottom: 3px;
+`;
+
+const RequiredMark = styled.span`
+  color: ${Colors.alert};
+`;
+
+const ConfirmButton = styled(Button)`
+  min-width: 95px;
+  width: 100%;
+  height: 32px;
+  font-size: 12px;
+  margin-top: 7px;
+`;
+
+const StyledNumberFieldWrapper = styled.div`
+  position: relative;
+
+  .MuiInputBase-input {
+    text-align: center;
+    width: 41px;
+    height: 17px;
+    padding: 1px calc(${p => p.$units.length}ch + 5px) 1px 3px;
+    font-size: 11px;
+
+    /* Remove the spinner arrows */
+    &::-webkit-outer-spin-button,
+    &::-webkit-inner-spin-button {
+      -webkit-appearance: none;
+      margin: 0px;
+    }
+
+    /* For Firefox */
+    -moz-appearance: textfield;
+  }
+`;
+
+const InputSuffix = styled.span`
+  font-size: 11px;
+  position: absolute;
+  right: 3px;
+  top: 1.7px;
+`;
+
+const StyledTimePicker = styled(Field)`
+  margin-bottom: 7px;
+  .MuiInputBase-root {
+    font-size: 12px;
+    height: 32px;
+    width: 100%;
+    color: ${Colors.darkestText};
+    background-color: ${Colors.white};
+    .MuiButtonBase-root {
+      padding: 5px;
+    }
+    .MuiSvgIcon-root {
+      font-size: 14px;
+    }
+    .MuiInputBase-input {
+      text-transform: lowercase;
+    }
+    .MuiOutlinedInput-notchedOutline {
+      border-width: 1px !important;
+    }
+    &.Mui-focused .MuiOutlinedInput-notchedOutline {
+      border-color: ${Colors.primary} !important;
+    }
+    :not(.Mui-disabled):hover .MuiOutlinedInput-notchedOutline {
+      border-color: ${Colors.softText};
+    }
+  }
+
+  /* Add error message styling */
+  .error-message {
+    color: ${Colors.alert};
+    font-size: 10px;
+    margin-top: 2px;
+  }
+`;
+
+const FormContainer = styled.div`
+  padding: 11px 14px;
+  width: ${p => p.$width}px;
+`;
+
+const ErrorMessage = styled.div`
+  color: ${Colors.alert};
+  font-size: 12px;
+  margin: 4px 2px 2px;
+  font-weight: 500;
+  line-height: 15px;
+`;
+
+const MainScreen = ({ onGivenClick, onNotGivenClick }) => {
+  return (
+    <PopperContent>
+      <StyledButton onClick={onGivenClick} variant="outlined" $color={Colors.green}>
+        <TranslatedText stringId="medication.status.given" fallback="Given" />
+      </StyledButton>
+      <Divider color={Colors.outline} />
+      <StyledButton onClick={onNotGivenClick} variant="outlined" $color={Colors.alert}>
+        <TranslatedText stringId="medication.status.notGiven" fallback="Not given" />
+      </StyledButton>
+    </PopperContent>
+  );
+};
+
+const ReasonScreen = ({ reasonsNotGiven, onReasonSelect }) => {
+  return (
+    <PopperContent>
+      {reasonsNotGiven?.data?.map(reason => (
+        <Button key={reason.id} variant="outlined" onClick={() => onReasonSelect(reason.id)}>
+          <TranslatedText stringId={reason.name} fallback={reason.name} />
+        </Button>
+      ))}
+    </PopperContent>
+  );
+};
+
+const GivenScreen = ({
+  doseAmount: prescriptionDoseAmount,
+  timeSlot,
+  selectedDate,
+  marId,
+  units,
+  onClose,
+  prescriptionId,
+  isFuture,
+  isPast,
+  isVariableDose,
+}) => {
+  const queryClient = useQueryClient();
+  const { encounter } = useEncounter();
+  const [containerWidth, setContainerWidth] = useState(null);
+  const doseInputRef = useRef(null);
+  const [showWarningModal, setShowWarningModal] = useState(null);
+  // Measure the DoseContainer width when component mounts
+  useLayoutEffect(() => {
+    if (doseInputRef.current) {
+      setContainerWidth(doseInputRef.current.offsetWidth + 62);
+    }
+  }, []);
+
+  const { mutateAsync: updateMar } = useGivenMarMutation(marId, {
+    onSuccess: () => {
+      queryClient.invalidateQueries(['encounterMedication', encounter?.id]);
+      onClose();
+    },
+  });
+  const handleDecreaseDose = (doseAmount, setFieldValue) => {
+    if (doseAmount <= 0.25) return;
+    setFieldValue('doseAmount', doseAmount - 0.25);
+  };
+
+  const handleIncreaseDose = (doseAmount, setFieldValue) => {
+    if (!doseAmount) {
+      setFieldValue('doseAmount', 0.5);
+      return;
+    }
+    setFieldValue('doseAmount', doseAmount + 0.5);
+  };
+
+  const handleSubmit = async data => {
+    const { timeGiven, doseAmount } = data;
+    if (
+      Number(doseAmount) !== Number(prescriptionDoseAmount) &&
+      !showWarningModal &&
+      !isVariableDose
+    ) {
+      setShowWarningModal(MAR_WARNING_MODAL.NOT_MATCHING_DOSE);
+      return;
+    }
+
+    const givenTime = set(new Date(selectedDate), {
+      hours: timeGiven.getHours(),
+      minutes: timeGiven.getMinutes(),
+      seconds: timeGiven.getSeconds(),
+    });
+    const dueAt = addHours(getDateFromTimeString(timeSlot.startTime, selectedDate), 1);
+    await updateMar({
+      dueAt,
+      prescriptionId,
+      dose: {
+        doseAmount,
+        givenTime,
+      },
+    });
+  };
+
+  return (
+    <Form
+      suppressErrorDialog
+      onSubmit={handleSubmit}
+      render={({ setFieldValue, values, submitForm, errors }) => (
+        <>
+          <WarningModal
+            modal={showWarningModal}
+            onClose={() => setShowWarningModal(null)}
+            onConfirm={() => {
+              setShowWarningModal(null);
+              handleSubmit(values);
+            }}
+          />
+          <FormContainer $width={containerWidth}>
+            <DoseContainer>
+              <DoseButton onClick={() => handleDecreaseDose(values.doseAmount, setFieldValue)}>
+                <RemoveCircleOutlineIcon />
+              </DoseButton>
+              <StyledNumberFieldWrapper $units={units} ref={doseInputRef}>
+                <Field name="doseAmount" component={NumberField} min={0.25} />
+                <InputSuffix>{units}</InputSuffix>
+              </StyledNumberFieldWrapper>
+              <DoseButton onClick={() => handleIncreaseDose(values.doseAmount, setFieldValue)}>
+                <AddCircleOutlineIcon />
+              </DoseButton>
+            </DoseContainer>
+
+            <TimeGivenTitle>
+              <TranslatedText stringId="medication.mar.timeGiven.label" fallback="Time given" />
+              <RequiredMark>*</RequiredMark>
+            </TimeGivenTitle>
+            <StyledTimePicker
+              name="timeGiven"
+              onChange={value => {
+                setFieldValue('timeGiven', value);
+              }}
+              component={TimePickerField}
+              format="hh:mmaa"
+              timeSteps={{ minutes: 1 }}
+              error={errors.timeGiven}
+              slotProps={{
+                textField: {
+                  readOnly: true,
+                  InputProps: {
+                    placeholder: '--:-- --',
+                  },
+                  error: errors.timeGiven,
+                },
+                digitalClockSectionItem: {
+                  sx: { fontSize: '14px' },
+                },
+              }}
+            />
+            {errors.timeGiven && <ErrorMessage>{errors.timeGiven}</ErrorMessage>}
+
+            <div>
+              <ConfirmButton
+                onClick={submitForm}
+                variant="outlined"
+                size="small"
+                disabled={!values.doseAmount}
+              >
+                <TranslatedText stringId="general.action.confirm" fallback="Confirm" />
+              </ConfirmButton>
+            </div>
+          </FormContainer>
+        </>
+      )}
+      initialValues={{
+        doseAmount: Number(prescriptionDoseAmount) || '',
+        timeGiven: isPast
+          ? addHours(getDateFromTimeString(timeSlot.startTime, selectedDate), 1)
+          : new Date(),
+      }}
+      validationSchema={yup.object().shape({
+        doseAmount: yup
+          .number()
+          .required(<TranslatedText stringId="validation.required.inline" fallback="*Required" />)
+          .translatedLabel(
+            <TranslatedText stringId="medication.mar.doseAmount.label" fallback="Dose amount" />,
+          ),
+        timeGiven: yup
+          .date()
+          .required(<TranslatedText stringId="validation.required.inline" fallback="*Required" />)
+          .test(
+            'time-within-slot',
+            <TranslatedText
+              stringId="medication.mar.timeGiven.validation.outside"
+              fallback="Time is outside selected window"
+            />,
+            function(value) {
+              if (!value || !timeSlot || isFuture) return true; // Skip validation if no value or timeSlot or isFuture
+
+              // Convert times to minutes since midnight for easier comparison
+              const timeToMinutes = date => date.getHours() * 60 + date.getMinutes();
+
+              // Get the minutes for the selected time
+              const selectedTimeMinutes = timeToMinutes(new Date(value));
+
+              const slotStartMinutes = timeToMinutes(getDateFromTimeString(timeSlot.startTime));
+              const slotEndMinutes = timeToMinutes(getDateFromTimeString(timeSlot.endTime));
+
+              // Check if the time slot crosses midnight
+              if (slotEndMinutes < slotStartMinutes) {
+                // For time slots that cross midnight
+                return (
+                  selectedTimeMinutes >= slotStartMinutes || selectedTimeMinutes <= slotEndMinutes
+                );
+              } else {
+                // For normal time slots within the same day
+                return (
+                  selectedTimeMinutes >= slotStartMinutes && selectedTimeMinutes <= slotEndMinutes
+                );
+              }
+            },
+          ),
+      })}
+    />
+  );
+};
+
+export const StatusPopper = ({
+  open,
+  anchorEl,
+  onClose,
+  timeSlot,
+  selectedDate,
+  marInfo,
+  medication,
+  isFuture,
+  isPast,
+}) => {
+  const { id: marId } = marInfo || {};
+  const { doseAmount, units, id: prescriptionId, isVariableDose } = medication || {};
+
   const [showReasonScreen, setShowReasonScreen] = useState(false);
-  const { mutateAsync: updateMar } = useNotGivenMarMutation(marId);
+  const [showGivenScreen, setShowGivenScreen] = useState(false);
+
+  const { mutateAsync: updateMar } = useNotGivenMarMutation(marId, {
+    onSuccess: () => {
+      queryClient.invalidateQueries(['encounterMedication', encounter?.id]);
+    },
+  });
   const queryClient = useQueryClient();
+
   const handleNotGivenClick = () => {
     setShowReasonScreen(true);
   };
+
   const { encounter } = useEncounter();
 
   const handleClose = () => {
     setShowReasonScreen(false);
+    setShowGivenScreen(false);
     onClose();
   };
 
   const handleReasonSelect = async reasonNotGivenId => {
+    const dueAt = addHours(getDateFromTimeString(timeSlot.startTime, selectedDate), 1);
     await updateMar({
       status: ADMINISTRATION_STATUS.NOT_GIVEN,
       reasonNotGivenId,
@@ -79,49 +443,37 @@ export const StatusPopper = ({ marId, open, anchorEl, onClose, dueAt, prescripti
       prescriptionId,
     });
 
-    queryClient.invalidateQueries(['encounterMedication', encounter?.id]);
-
     setShowReasonScreen(false);
     handleClose();
   };
 
   const handleGivenClick = () => {
-    handleClose();
+    setShowGivenScreen(true);
   };
 
   const reasonsNotGiven = useSuggestionsQuery('reasonNotGiven');
 
-  const renderMainScreen = () => {
-    return (
-      <PopperContent>
-        <StyledButton onClick={handleGivenClick} variant="outlined" $color={Colors.green}>
-          <TranslatedText stringId="medication.status.given" fallback="Given" />
-        </StyledButton>
-        <Divider color={Colors.outline} />
-        <StyledButton onClick={handleNotGivenClick} variant="outlined" $color={Colors.alert}>
-          <TranslatedText stringId="medication.status.notGiven" fallback="Not given" />
-        </StyledButton>
-      </PopperContent>
-    );
-  };
-
-  const renderReasonScreen = () => {
-    return (
-      <PopperContent>
-        {reasonsNotGiven?.data?.map(reason => (
-          <Button key={reason.id} variant="outlined" onClick={() => handleReasonSelect(reason.id)}>
-            <TranslatedText stringId={reason.name} fallback={reason.name} />
-          </Button>
-        ))}
-      </PopperContent>
-    );
-  };
-
   const getContent = () => {
     if (showReasonScreen) {
-      return renderReasonScreen();
+      return <ReasonScreen reasonsNotGiven={reasonsNotGiven} onReasonSelect={handleReasonSelect} />;
+    }
+    if (showGivenScreen) {
+      return (
+        <GivenScreen
+          doseAmount={doseAmount}
+          timeSlot={timeSlot}
+          selectedDate={selectedDate}
+          units={units}
+          marId={marId}
+          onClose={onClose}
+          prescriptionId={prescriptionId}
+          isFuture={isFuture}
+          isPast={isPast}
+          isVariableDose={isVariableDose}
+        />
+      );
     }
-    return renderMainScreen();
+    return <MainScreen onGivenClick={handleGivenClick} onNotGivenClick={handleNotGivenClick} />;
   };
 
   return (
@@ -136,6 +488,15 @@ export const StatusPopper = ({ marId, open, anchorEl, onClose, dueAt, prescripti
           offset: '0, 10',
         },
       }}
+      popperOptions={{
+        positionFixed: true,
+        modifiers: {
+          preventOverflow: {
+            enabled: true,
+            boundariesElement: 'window',
+          },
+        },
+      }}
       style={{ zIndex: 1300 }}
     >
       {({ TransitionProps }) => (
diff --git a/packages/web/app/components/Medication/WarningModal.jsx b/packages/web/app/components/Medication/WarningModal.jsx
index 4cc37326ac3..95b5d0dbe34 100644
--- a/packages/web/app/components/Medication/WarningModal.jsx
+++ b/packages/web/app/components/Medication/WarningModal.jsx
@@ -73,6 +73,20 @@ export const WarningModal = ({ modal, onClose, onConfirm, isPast = false }) => {
         />
       );
       break;
+    case MAR_WARNING_MODAL.NOT_MATCHING_DOSE:
+      title = (
+        <TranslatedText
+          stringId="medication.mar.notMatchingDose.warning.title"
+          fallback="Dose amount does not match prescription" 
+        />
+      );
+      text = (
+        <TranslatedText
+          stringId="medication.mar.notMatchingDose.warning.text"
+          fallback="The dose amount recorded does not match the prescribed dose amount. Please confirm that you would like to continue recording the dose."
+        />
+      );
+      break;
   }
 
   return (
diff --git a/packages/web/app/constants/medication.js b/packages/web/app/constants/medication.js
index cc0323085fb..c5ebd0b63ed 100644
--- a/packages/web/app/constants/medication.js
+++ b/packages/web/app/constants/medication.js
@@ -2,4 +2,5 @@ export const MAR_WARNING_MODAL = {
   PAUSED: 'paused',
   FUTURE: 'future',
   PAST: 'past',
+  NOT_MATCHING_DOSE: 'notMatchingDose',
 };


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T17:20:55Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**packages/web/app/components/Medication/MarStatus.jsx:338**
* The variable 'units' is used without being declared or extracted; ensure it is properly defined from the medication props.
```
doses: `${firstDose?.doseAmount} ${units}`,
```
**packages/database/src/migrations/1742897789276-addMedicationAdministrationRecordDoseTable.ts:15**
* DataTypes.DATETIMESTRING is not a valid Sequelize data type; replace it with DataTypes.DATE (or the appropriate date type) to avoid runtime errors.
```
given_time: { type: DataTypes.DATETIMESTRING, allowNull: false, },
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T17:14:24Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**packages/database/src/migrations/1742897789276-addMedicationAdministrationRecordDoseTable.ts:15**
* The data type 'DATETIMESTRING' is nonstandard; consider using DataTypes.DATE for consistency with similar timestamp fields.
```
given_time: { type: DataTypes.DATETIMESTRING,
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T17:32:06Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T08:12:21Z
user: login: passcod
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T02:03:28Z
user: login: passcod
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T02:03:57Z
user: login: passcod
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T02:06:23Z
user: login: passcod
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T02:07:38Z
user: login: passcod
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T02:09:04Z
user: login: passcod
body: a few details but otherwith 👍🏻 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-10T02:09:35Z
user: login: khoi-bes
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T07:27:33Z

Review comments:
diff_hunk: @@ -0,0 +1,47 @@
+import { DataTypes, QueryInterface } from 'sequelize';
+
+export async function up(query: QueryInterface): Promise<void> {
+  await query.createTable('medication_administration_record_doses', {
+    id: {
+      type: DataTypes.UUID,
+      allowNull: false,
+      primaryKey: true,
+    },
+    dose_amount: {
+      type: DataTypes.DECIMAL,
+      allowNull: false,
+    },
+    given_time: {
+      type: DataTypes.DATETIMESTRING,
path: packages/database/src/migrations/1742897789276-addMedicationAdministrationRecordDoseTable.ts
user: login: Copilot
body: DataTypes.DATETIMESTRING is not a standard Sequelize type; consider using DataTypes.DATE to ensure proper date handling.
```suggestion
      type: DataTypes.DATE,
```
created_at: 2025-04-07T17:20:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -451,8 +449,101 @@ medication.get(
   }),
 );
 
-medication.put('/mar/:id', simplePut('MedicationAdministrationRecord'));
-medication.post('/mar', simplePost('MedicationAdministrationRecord'));
+medication.put(
+  '/mar/:id',
+  asyncHandler(async (req, res) => {
+    const { models, params } = req;
+    const { MedicationAdministrationRecord, MedicationAdministrationRecordDose } = models;
+    
+    // Start a transaction
+    const transaction = await req.db.transaction();
+    
+    try {
+      // Get the MAR
+      const mar = await MedicationAdministrationRecord.findByPk(params.id);
+      if (!mar) throw new NotFoundError();
+      req.checkPermission('write', mar);
+      
+      // Update basic fields
+      const { doses, ...marData } = req.body;
+      await mar.update(marData, { transaction });
+      
+      // Handle doses using bulkCreate
+      if (doses && Array.isArray(doses) && doses.length > 0) {
+        // Prepare the doses with the MAR ID
+        const dosesWithMarId = doses.map(dose => ({
+          ...dose,
+          medicationAdministrationRecordId: mar.id
+        }));
+        
+        // Use bulkCreate for better performance
+        await MedicationAdministrationRecordDose.bulkCreate(dosesWithMarId, { transaction });
path: packages/facility-server/app/routes/apiv1/medication.js
user: login: Copilot
body: When updating a MAR record, existing dose records are not removed prior to bulk creation, which may result in duplicate dose entries. Consider clearing existing doses or updating them instead of always bulk creating new records.
created_at: 2025-04-07T17:20:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,6 +324,24 @@ export const MarStatus = ({
               <div>{reasonNotGiven?.name}</div>
             </>
           );
+        case ADMINISTRATION_STATUS.GIVEN: {
+          const firstDose = doses?.[0];
+          if (!firstDose) return null;
+          console.log('firstDose', firstDose);
+          console.log('doses', doses);
path: packages/web/app/components/Medication/MarStatus.jsx
user: login: Copilot
body: Remove the debugging console.log statement to avoid exposing internal state in production.
```suggestion
          // console.log('firstDose', firstDose);
          // console.log('doses', doses);
```
created_at: 2025-04-08T17:14:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,6 +324,24 @@ export const MarStatus = ({
               <div>{reasonNotGiven?.name}</div>
             </>
           );
+        case ADMINISTRATION_STATUS.GIVEN: {
+          const firstDose = doses?.[0];
+          if (!firstDose) return null;
+          console.log('firstDose', firstDose);
+          console.log('doses', doses);
path: packages/web/app/components/Medication/MarStatus.jsx
user: login: Copilot
body: Remove debugging console.log statements from production code.
```suggestion

```
created_at: 2025-04-08T17:32:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,6 +324,24 @@ export const MarStatus = ({
               <div>{reasonNotGiven?.name}</div>
             </>
           );
+        case ADMINISTRATION_STATUS.GIVEN: {
+          const firstDose = doses?.[0];
+          if (!firstDose) return null;
+          console.log('firstDose', firstDose);
+          console.log('doses', doses);
path: packages/web/app/components/Medication/MarStatus.jsx
user: login: Copilot
body: Remove debugging console.log statements from production code.
```suggestion

```
created_at: 2025-04-08T17:32:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -458,6 +458,89 @@ medication.get(
   }),
 );
 
+const givenMarUpdateSchema = z.object({
+  dose: z.object({
+    doseAmount: z.number(),
+    givenTime: z.string(),
path: packages/facility-server/app/routes/apiv1/medication.js
user: login: Copilot
body: The schema for 'givenTime' in the update endpoint uses z.string() while the create endpoint uses z.string().datetime(); consider aligning these validations for consistency.
```suggestion
    givenTime: z.string().datetime(),
```
created_at: 2025-04-09T08:12:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+import { DataTypes, QueryInterface } from 'sequelize';
+
+export async function up(query: QueryInterface): Promise<void> {
+  await query.createTable('medication_administration_record_doses', {
+    id: {
+      type: DataTypes.UUID,
+      allowNull: false,
+      primaryKey: true,
+    },
+    dose_amount: {
+      type: DataTypes.DECIMAL,
+      allowNull: false,
+    },
+    given_time: {
+      type: DataTypes.DATETIMESTRING,
path: packages/database/src/migrations/1742897789276-addMedicationAdministrationRecordDoseTable.ts
user: login: Copilot
body: DataTypes.DATETIMESTRING is non-standard; if the intention is to store a date-time value, consider using DataTypes.DATE to ensure proper type handling.
```suggestion
      type: DataTypes.DATE,
```
created_at: 2025-04-09T08:12:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+import { DataTypes, QueryInterface } from 'sequelize';
+
+export async function up(query: QueryInterface): Promise<void> {
+  await query.createTable('medication_administration_record_doses', {
+    id: {
+      type: DataTypes.UUID,
+      allowNull: false,
+      primaryKey: true,
path: packages/database/src/migrations/1742897789276-addMedicationAdministrationRecordDoseTable.ts
user: login: passcod
body: ```suggestion
      primaryKey: true,
      defaultValue: Sequelize.fn('gen_random_uuid'),
```
created_at: 2025-04-10T02:03:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,47 @@
+import { DataTypes, QueryInterface } from 'sequelize';
+
+export async function up(query: QueryInterface): Promise<void> {
+  await query.createTable('medication_administration_record_doses', {
+    id: {
+      type: DataTypes.UUID,
+      allowNull: false,
+      primaryKey: true,
+    },
+    dose_amount: {
+      type: DataTypes.DECIMAL,
+      allowNull: false,
+    },
+    given_time: {
+      type: DataTypes.DATETIMESTRING,
+      allowNull: false,
+    },
+    given_by_user_id: {
+      type: DataTypes.STRING,
+      allowNull: true,
+    },
+    recorded_by_user_id: {
+      type: DataTypes.STRING,
+      allowNull: true,
+    },
+    medication_administration_record_id: {
+      type: DataTypes.STRING,
+      allowNull: false,
+    },
+    created_at: {
+      type: DataTypes.DATE,
+      allowNull: false,
+    },
+    updated_at: {
+      type: DataTypes.DATE,
+      allowNull: false,
+    },
path: packages/database/src/migrations/1742897789276-addMedicationAdministrationRecordDoseTable.ts
user: login: passcod
body: ```suggestion
    created_at: {
      type: DataTypes.DATE,
      allowNull: false,
      defaultValue: Sequelize.fn('now'),
    },
    updated_at: {
      type: DataTypes.DATE,
      allowNull: false,
      defaultValue: Sequelize.fn('now'),
    },
```
created_at: 2025-04-10T02:03:56Z
author_association: MEMBER
diff_hunk: @@ -203,7 +203,13 @@ encounterRelations.get(
         {
           model: models.MedicationAdministrationRecord,
           as: 'medicationAdministrationRecords',
-          include: 'reasonNotGiven'
+          include: [
+            'reasonNotGiven', 
+            {
+              association: 'doses',
+              order: [['createdAt', 'ASC']]  // Order doses by oldest first
path: packages/facility-server/app/routes/apiv1/encounter.js
user: login: passcod
body: this should use `givenTime` I think, `createdAt` is affected by sync so not reliable
created_at: 2025-04-10T02:06:23Z
author_association: MEMBER
diff_hunk: @@ -458,6 +458,89 @@ medication.get(
   }),
 );
 
+const givenMarUpdateSchema = z.object({
+  dose: z.object({
+    doseAmount: z.number(),
+    givenTime: z.string().datetime(),
+  }),
+});
+medication.put(
+  '/mar/:id/given',
path: packages/facility-server/app/routes/apiv1/medication.js
user: login: passcod
body: is MAR standard or something we've used elsewhere? would prefer the meaning be spelled out if not
created_at: 2025-04-10T02:07:38Z
author_association: MEMBER
diff_hunk: @@ -458,6 +458,89 @@ medication.get(
   }),
 );
 
+const givenMarUpdateSchema = z.object({
+  dose: z.object({
+    doseAmount: z.number(),
+    givenTime: z.string().datetime(),
+  }),
+});
+medication.put(
+  '/mar/:id/given',
path: packages/facility-server/app/routes/apiv1/medication.js
user: login: passcod
body: (in the route and also in exported functions elsewhere; local variables can remain short though something like `const record = await...` might be clearer.)
created_at: 2025-04-10T02:09:04Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-17T09:41:32Z
body: **Android builds 📱**
- **28152:** [Download Android APK](https://github.com/beyondessential/tamanu/actions/runs/14517469799/artifacts/2964120860) **Latest**
- **28146:** [Download Android APK](https://github.com/beyondessential/tamanu/actions/runs/14516801522/artifacts/2963878658)
- **28144:** [Download Android APK](https://github.com/beyondessential/tamanu/actions/runs/14516298222/artifacts/2963705732)
- **28142:** [Download Android APK](https://github.com/beyondessential/tamanu/actions/runs/14514203010/artifacts/2963023335)
- **28140:** [Download Android APK](https://github.com/beyondessential/tamanu/actions/runs/14512647341/artifacts/2962526110)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-17T09:47:16Z
body: #### :tropical_drink: `up` on tamanu-on-k8s/bes/tamanu-on-k8s/feature-epi-1128-build-workflow-to-record-a-medication-a

   <details>
   <summary>Pulumi report</summary>

   
   <pre>
   Updating (bes/feature-epi-1128-build-workflow-to-record-a-medication-a)

View Live: https://app.pulumi.com/bes/tamanu-on-k8s/feature-epi-1128-build-workflow-to-record-a-medication-a/updates/1

@ Updating....

@ Updating....
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (0s) 
@ Updating.....
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (1s) read pulumi:pulumi:StackReference bes/k8s-core/tamanu-internal-main
@ Updating....
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (2s) read pulumi:pulumi:StackReference bes/k8s-core/tamanu-internal-main
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (2s) Using tailscale proxy https://k8s-operator-tamanu-internal-main.tail53aef.ts.net
@ Updating....
+  bes:tamanu:FacilityServer 1 creating (0s) 
+  kubernetes:core/v1:Namespace tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a creating (0s) 
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (3s) read pulumi:pulumi:StackReference bes/core/tamanu-internal
+  bes:tamanu:FacilityServer 2 creating (0s) 
+  bes:tamanu:CentralServer central creating (0s) 
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (3s) read pulumi:pulumi:StackReference bes/core/tamanu-internal
+  bes:tamanu:WebFrontend facility-1 creating (0s) 
+  kubernetes:core/v1:Namespace tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a creating (0s) 
+  kubernetes:core/v1:Namespace tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a creating (0s) 
@ Updating....
+  bes:tamanu:WebFrontend facility-2 creating (0s) 
+  kubernetes:core/v1:Namespace tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a created (1s) 
+  bes:tamanu:WebFrontend central creating (0s) 
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (4s) Waiting for central-db...
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (4s) Waiting for facility-1-db...
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (4s) Waiting for facility-2-db...
+  kubernetes:core/v1:Secret mailgun creating (0s) 
@ Updating....
+  kubernetes:apps/v1:Deployment facility-1-web creating (0s) 
+  kubernetes:core/v1:ServiceAccount app-sa creating (0s) 
+  kubernetes:hnc.x-k8s.io/v1alpha2:HierarchyConfiguration parent-ns creating (0s) 
+  kubernetes:core/v1:Secret bugsnag creating (0s) 
+  kubernetes:core/v1:Secret pullsecret-github creating (0s) 
+  kubernetes:core/v1:Secret tupaia creating (0s) 
+  kubernetes:core/v1:ConfigMap provisioning creating (0s) 
+  kubernetes:core/v1:Service facility-1-api creating (0s) 
+  kubernetes:core/v1:Service central-api creating (0s) 
+  kubernetes:postgresql.cnpg.io/v1:Cluster facility-2-db creating (0s) 
+  kubernetes:networking.k8s.io/v1:Ingress bare-domain creating (0s) 
+  kubernetes:core/v1:Service facility-1-sync creating (0s) 
+  kubernetes:core/v1:Service facility-2-sync creating (0s) 
+  kubernetes:postgresql.cnpg.io/v1:Cluster central-db creating (0s) 
+  kubernetes:core/v1:Service facility-1-web creating (0s) 
+  kubernetes:core/v1:Secret mailgun creating (0s) 
+  kubernetes:core/v1:Secret mailgun creating (0s) 
+  kubernetes:core/v1:Secret mailgun created (0.66s) 
+  kubernetes:core/v1:ServiceAccount app-sa creating (0s) 
+  kubernetes:apps/v1:Deployment facility-1-web creating (0s) 
+  kubernetes:core/v1:ServiceAccount app-sa creating (0s) 
+  kubernetes:hnc.x-k8s.io/v1alpha2:HierarchyConfiguration parent-ns creating (0s) 
+  kubernetes:hnc.x-k8s.io/v1alpha2:HierarchyConfiguration parent-ns creating (0s) 
+  kubernetes:apps/v1:Deployment facility-1-web creating (0s) warning: [MinimumReplicasUnavailable] Deployment does not have minimum availability.
+  kubernetes:apps/v1:Deployment facility-1-web creating (0s) Waiting for app ReplicaSet to be available (0/2 Pods available)
@ Updating....
+  kubernetes:core/v1:ServiceAccount app-sa created (0.73s) 
+  kubernetes:core/v1:Secret bugsnag creating (0s) 
+  kubernetes:core/v1:Secret bugsnag creating (0s) 
+  kubernetes:hnc.x-k8s.io/v1alpha2:HierarchyConfiguration parent-ns created (0.84s) 
+  kubernetes:core/v1:Secret pullsecret-github creating (0s) 
+  kubernetes:core/v1:Secret pullsecret-github creating (0s) 
+  kubernetes:core/v1:Secret tupaia creating (0s) 
+  kubernetes:core/v1:Secret tupaia creating (0s) 
+  kubernetes:core/v1:ConfigMap provisioning creating (0s) 
+  kubernetes:core/v1:ConfigMap provisioning creating (0s) 
+  kubernetes:core/v1:Secret bugsnag created (0.98s) 
+  kubernetes:core/v1:Secret pullsecret-github created (0.99s) 
+  kubernetes:core/v1:Secret tupaia created (1s) 
+  kubernetes:core/v1:Service facility-1-api creating (1s) 
+  kubernetes:core/v1:Service central-api creating (1s) 
+  kubernetes:core/v1:ConfigMap provisioning created (1s) 
+  kubernetes:postgresql.cnpg.io/v1:Cluster facility-2-db creating (1s) 
+  kubernetes:core/v1:Service facility-1-api creating (1s) [1/3] Finding Pods to direct traffic to
+  kubernetes:postgresql.cnpg.io/v1:Cluster facility-2-db creating (1s) 
+  kubernetes:core/v1:Service central-api creating (1s) [1/3] Finding Pods to direct traffic to
+  kubernetes:postgresql.cnpg.io/v1:Cluster facility-2-db created (1s) 
+  kubernetes:core/v1:Service facility-1-sync creating (1s) 
+  kubernetes:networking.k8s.io/v1:Ingress bare-domain creating (1s) 
+  kubernetes:networking.k8s.io/v1:Ingress bare-domain creating (1s) [1/3] Finding a matching service for each Ingress path
+  kubernetes:networking.k8s.io/v1:Ingress bare-domain creating (1s) [2/3] Waiting for update of .status.loadBalancer with hostname/IP
+  kubernetes:core/v1:Service facility-2-sync creating (1s) 
+  kubernetes:core/v1:Service facility-1-sync creating (1s) [1/3] Finding Pods to direct traffic to
+  kubernetes:core/v1:ConfigMap central creating (0s) 
+  kubernetes:core/v1:Service facility-2-sync creating (1s) [1/3] Finding Pods to direct traffic to
+  kubernetes:postgresql.cnpg.io/v1:Cluster central-db creating (1s) 
+  kubernetes:postgresql.cnpg.io/v1:Cluster central-db creating (1s) 
@ Updating....
+  kubernetes:postgresql.cnpg.io/v1:Cluster facility-1-db creating (0s) 
+  kubernetes:core/v1:Service facility-1-web creating (1s) 
+  kubernetes:postgresql.cnpg.io/v1:Cluster central-db created (1s) 
+  kubernetes:core/v1:Service facility-2-api creating (0s) 
+  kubernetes:core/v1:Service facility-1-web creating (1s) [1/3] Finding Pods to direct traffic to
+  kubernetes:core/v1:Service facility-1-web creating (1s) 
+  kubernetes:core/v1:Service facility-2-web creating (0s) 
+  kubernetes:apps/v1:Deployment facility-2-web creating (0s) 
+  kubernetes:core/v1:Service facility-1-web created (2s) 
+  kubernetes:core/v1:Service central-web creating (0s) 
+  kubernetes:apps/v1:Deployment central-web creating (0s) 
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (7s) read kubernetes:core/v1:ConfigMap actual-provisioning
+  kubernetes:core/v1:ConfigMap central creating (0s) 
+  kubernetes:core/v1:ConfigMap central creating (0s) 
+  kubernetes:postgresql.cnpg.io/v1:Cluster facility-1-db creating (0s) 
+  kubernetes:postgresql.cnpg.io/v1:Cluster facility-1-db creating (0s) 
@ Updating....
+  kubernetes:core/v1:ConfigMap central created (1s) 
+  kubernetes:postgresql.cnpg.io/v1:Cluster facility-1-db created (1s) 
+  kubernetes:core/v1:Service facility-2-api creating (1s) 
+  kubernetes:core/v1:Service facility-2-web creating (1s) 
+  kubernetes:apps/v1:Deployment facility-2-web creating (1s) 
+  kubernetes:core/v1:Service facility-2-api creating (1s) [1/3] Finding Pods to direct traffic to
+  kubernetes:core/v1:Service facility-2-web creating (1s) [1/3] Finding Pods to direct traffic to
+  kubernetes:core/v1:Service facility-2-web creating (1s) 
+  kubernetes:apps/v1:Deployment facility-2-web creating (1s) warning: [MinimumReplicasUnavailable] Deployment does not have minimum availability.
+  kubernetes:core/v1:Service central-web creating (1s) 
+  kubernetes:apps/v1:Deployment central-web creating (1s) 
+  kubernetes:core/v1:Service facility-2-web created (1s) 
@ Updating....
+  kubernetes:apps/v1:Deployment facility-2-web creating (1s) Waiting for app ReplicaSet to be available (0/2 Pods available)
+  kubernetes:core/v1:Service central-web creating (1s) [1/3] Finding Pods to direct traffic to
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (9s) read kubernetes:core/v1:ConfigMap actual-provisioning
+  kubernetes:core/v1:Service central-web creating (1s) 
+  kubernetes:apps/v1:Deployment central-web creating (1s) warning: [MinimumReplicasUnavailable] Deployment does not have minimum availability.
+  kubernetes:apps/v1:Deployment central-web creating (1s) Waiting for app ReplicaSet to be available (0/2 Pods available)
+  kubernetes:core/v1:Service central-web created (1s) 
+  kubernetes:core/v1:ConfigMap facility-1 creating (0s) 
+  kubernetes:core/v1:ConfigMap facility-2 creating (0s) 
+  kubernetes:core/v1:ConfigMap facility-1 creating (0s) 
+  kubernetes:core/v1:ConfigMap facility-1 creating (0s) 
@ Updating....
+  kubernetes:core/v1:ConfigMap facility-2 creating (0s) 
+  kubernetes:core/v1:ConfigMap facility-2 creating (0s) 
+  kubernetes:core/v1:ConfigMap facility-1 created (0.52s) 
+  kubernetes:core/v1:ConfigMap facility-2 created (0.73s) 
@ Updating........
+  kubernetes:apps/v1:Deployment facility-1-web creating (10s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-web-7bc9d747-57dbcb56cb-kw7pg]: containers with unready status: [http]
+  kubernetes:apps/v1:Deployment facility-1-web creating (10s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-web-7bc9d747-57dbcb56cb-tfdw6]: containers with unready status: [http]
@ Updating....
+  kubernetes:core/v1:Service facility-1-api creating (11s) Service initialization complete
+  kubernetes:core/v1:Service facility-1-api creating (11s) 
+  kubernetes:core/v1:Service central-api creating (11s) Service initialization complete
+  kubernetes:core/v1:Service central-api creating (11s) 
+  kubernetes:core/v1:Service facility-1-api created (11s) 
+  kubernetes:networking.k8s.io/v1:Ingress facility-1 creating (0s) 
+  kubernetes:core/v1:Service central-api created (11s) 
+  kubernetes:core/v1:Service facility-1-sync creating (11s) Service initialization complete
+  kubernetes:core/v1:Service facility-1-sync creating (11s) 
@ Updating....
+  kubernetes:core/v1:Service facility-2-sync creating (11s) Service initialization complete
+  kubernetes:core/v1:Service facility-2-sync creating (11s) 
+  kubernetes:networking.k8s.io/v1:Ingress central creating (0s) 
+  kubernetes:core/v1:Service facility-1-sync created (11s) 
+  kubernetes:networking.k8s.io/v1:Ingress facility-1 creating (0s) 
+  kubernetes:networking.k8s.io/v1:Ingress facility-1 creating (0s) [1/3] Finding a matching service for each Ingress path
+  kubernetes:core/v1:Service facility-2-sync created (11s) 
+  kubernetes:networking.k8s.io/v1:Ingress facility-1 creating (0s) [2/3] Waiting for update of .status.loadBalancer with hostname/IP
@ Updating....
+  kubernetes:apps/v1:Deployment facility-2-web creating (11s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-2-web-c9310a16-786db4b56f-tmjdz]: containers with unready status: [http]
+  kubernetes:apps/v1:Deployment facility-2-web creating (11s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-2-web-c9310a16-786db4b56f-ql72c]: containers with unready status: [http]
+  kubernetes:core/v1:Service facility-2-api creating (11s) Service initialization complete
+  kubernetes:core/v1:Service facility-2-api creating (11s) 
+  kubernetes:apps/v1:Deployment central-web creating (11s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-web-1e092b48-6867d86758-tj7z5]: containers with unready status: [http]
+  kubernetes:apps/v1:Deployment central-web creating (11s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-web-1e092b48-6867d86758-87xcs]: containers with unready status: [http]
@ Updating....
+  kubernetes:networking.k8s.io/v1:Ingress central creating (2s) 
+  kubernetes:networking.k8s.io/v1:Ingress central creating (2s) [1/3] Finding a matching service for each Ingress path
+  kubernetes:core/v1:Service facility-2-api created (12s) 
+  kubernetes:networking.k8s.io/v1:Ingress central creating (2s) [2/3] Waiting for update of .status.loadBalancer with hostname/IP
+  kubernetes:networking.k8s.io/v1:Ingress facility-2 creating (0s) 
@ Updating....
+  kubernetes:apps/v1:Deployment facility-2-web creating (12s) Waiting for app ReplicaSet to be available (1/2 Pods available)
+  kubernetes:apps/v1:Deployment facility-1-web creating (15s) Waiting for app ReplicaSet to be available (1/2 Pods available)
+  kubernetes:apps/v1:Deployment central-web creating (13s) Waiting for app ReplicaSet to be available (1/2 Pods available)
+  kubernetes:networking.k8s.io/v1:Ingress facility-2 creating (1s) 
+  kubernetes:networking.k8s.io/v1:Ingress facility-2 creating (1s) [1/3] Finding a matching service for each Ingress path
@ Updating....
+  kubernetes:apps/v1:Deployment central-web creating (14s) Deployment initialization complete
+  kubernetes:apps/v1:Deployment central-web creating (14s) 
+  kubernetes:networking.k8s.io/v1:Ingress facility-2 creating (2s) [2/3] Waiting for update of .status.loadBalancer with hostname/IP
+  kubernetes:apps/v1:Deployment central-web created (14s) 
@ Updating....
+  kubernetes:apps/v1:Deployment facility-1-web creating (17s) Deployment initialization complete
+  kubernetes:apps/v1:Deployment facility-1-web creating (17s) 
+  kubernetes:apps/v1:Deployment facility-2-web creating (14s) Deployment initialization complete
+  kubernetes:apps/v1:Deployment facility-2-web creating (14s) 
+  kubernetes:apps/v1:Deployment facility-1-web created (17s) 
+  kubernetes:apps/v1:Deployment facility-2-web created (15s) 
@ Updating..................................
+  kubernetes:networking.k8s.io/v1:Ingress facility-1 creating (36s) Ingress initialization complete
+  kubernetes:networking.k8s.io/v1:Ingress facility-1 creating (36s) 
+  kubernetes:networking.k8s.io/v1:Ingress facility-2 creating (33s) Ingress initialization complete
+  kubernetes:networking.k8s.io/v1:Ingress facility-2 creating (33s) 
+  kubernetes:networking.k8s.io/v1:Ingress central creating (36s) Ingress initialization complete
+  kubernetes:networking.k8s.io/v1:Ingress bare-domain creating (47s) Ingress initialization complete
+  kubernetes:networking.k8s.io/v1:Ingress central creating (36s) 
+  kubernetes:networking.k8s.io/v1:Ingress bare-domain creating (47s) 
+  kubernetes:networking.k8s.io/v1:Ingress facility-1 created (36s) 
+  kubernetes:networking.k8s.io/v1:Ingress facility-2 created (33s) 
+  kubernetes:networking.k8s.io/v1:Ingress bare-domain created (48s) 
+  kubernetes:networking.k8s.io/v1:Ingress central created (36s) 
@ Updating...............................................................................................................
+  kubernetes:batch/v1:Job central-migrator creating (0s) 
+  kubernetes:batch/v1:Job facility-1-migrator creating (0s) 
@ Updating....
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (162s) Secret central-db-superuser not found or not ready: Error: HTTP-Code: 404
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (162s) Message: Unknown API Status Code!
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (162s) Body: "{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"secrets \\\"central-db-superuser\\\" not found\",\"reason\":\"NotFound\",\"details\":{\"name\":\"central-db-superuser\",\"kind\":\"secrets\"},\"code\":404}
"
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (162s) Headers: {"audit-id":"ecdd088d-cda7-48ee-b859-377eae70e67e","cache-control":"no-cache, private","connection":"close","content-length":"214","content-type":"application/json","date":"Thu, 17 Apr 2025 14:08:39 GMT","x-kubernetes-pf-flowschema-uid":"7d46608e-fb55-48cf-90aa-981c856b1d54","x-kubernetes-pf-prioritylevel-uid":"ed197318-138c-44ea-825e-1d4df495928e"}
+  kubernetes:core/v1:Secret central-db-url creating (0s) 
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (162s) Secret facility-1-db-superuser not found or not ready: Error: HTTP-Code: 404
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (162s) Message: Unknown API Status Code!
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (162s) Body: "{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"secrets \\\"facility-1-db-superuser\\\" not found\",\"reason\":\"NotFound\",\"details\":{\"name\":\"facility-1-db-superuser\",\"kind\":\"secrets\"},\"code\":404}
"
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (162s) Headers: {"audit-id":"7504af11-6396-498d-a04f-22b00a24d476","cache-control":"no-cache, private","connection":"close","content-length":"220","content-type":"application/json","date":"Thu, 17 Apr 2025 14:08:39 GMT","x-kubernetes-pf-flowschema-uid":"7d46608e-fb55-48cf-90aa-981c856b1d54","x-kubernetes-pf-prioritylevel-uid":"ed197318-138c-44ea-825e-1d4df495928e"}
+  kubernetes:core/v1:Secret facility-1-db-url creating (0s) 
+  kubernetes:batch/v1:Job facility-2-migrator creating (0s) 
+  kubernetes:batch/v1:Job central-migrator creating (0s) 
+  kubernetes:batch/v1:Job facility-1-migrator creating (0s) 
+  kubernetes:core/v1:Secret central-db-url creating (0s) 
+  kubernetes:core/v1:Secret central-db-url creating (0s) 
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (163s) Secret facility-2-db-superuser not found or not ready: Error: HTTP-Code: 404
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (163s) Message: Unknown API Status Code!
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (163s) Body: "{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"secrets \\\"facility-2-db-superuser\\\" not found\",\"reason\":\"NotFound\",\"details\":{\"name\":\"facility-2-db-superuser\",\"kind\":\"secrets\"},\"code\":404}
"
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a creating (163s) Headers: {"audit-id":"7e007abe-7ed9-429d-8914-da820488d5d9","cache-control":"no-cache, private","connection":"close","content-length":"220","content-type":"application/json","date":"Thu, 17 Apr 2025 14:08:39 GMT","x-kubernetes-pf-flowschema-uid":"7d46608e-fb55-48cf-90aa-981c856b1d54","x-kubernetes-pf-prioritylevel-uid":"ed197318-138c-44ea-825e-1d4df495928e"}
+  kubernetes:core/v1:Secret facility-2-db-url creating (0s) 
+  kubernetes:core/v1:Secret central-db-url created (0.62s) 
+  kubernetes:core/v1:Secret facility-1-db-url creating (0s) 
+  kubernetes:core/v1:Secret facility-1-db-url creating (0s) 
+  kubernetes:batch/v1:Job facility-2-migrator creating (0s) 
@ Updating....
+  kubernetes:batch/v1:Job central-migrator creating (1s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-migrator-ffc07834" to start
+  kubernetes:batch/v1:Job central-migrator creating (1s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-migrator-ffc07834" to succeed (Active: 1 | Succeeded: 0 | Failed: 0)
+  kubernetes:batch/v1:Job facility-1-migrator creating (1s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-migrator-10decd25" to start
+  kubernetes:batch/v1:Job facility-1-migrator creating (1s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-migrator-10decd25" to succeed (Active: 1 | Succeeded: 0 | Failed: 0)
+  kubernetes:core/v1:Secret facility-1-db-url created (0.71s) 
+  kubernetes:batch/v1:Job facility-2-migrator creating (0s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-2-migrator-9639626f" to start
+  kubernetes:batch/v1:Job facility-2-migrator creating (0s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-2-migrator-9639626f" to succeed (Active: 1 | Succeeded: 0 | Failed: 0)
+  kubernetes:core/v1:Secret facility-2-db-url creating (0s) 
+  kubernetes:core/v1:Secret facility-2-db-url creating (0s) 
+  kubernetes:core/v1:Secret facility-2-db-url created (0.48s) 
@ Updating.........................................
+  kubernetes:batch/v1:Job facility-1-migrator creating (39s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-migrator-10decd25-v8gc4]: Container "migrator" completed with exit code 0
@ Updating.....
+  kubernetes:batch/v1:Job facility-1-migrator creating (41s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-migrator-10decd25" to succeed (Active: 0 | Succeeded: 0 | Failed: 0)
+  kubernetes:batch/v1:Job facility-1-migrator creating (41s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-migrator-10decd25" to succeed (Active: 0 | Succeeded: 1 | Failed: 0)
+  kubernetes:batch/v1:Job facility-1-migrator creating (41s) 
+  kubernetes:batch/v1:Job facility-1-migrator created (41s) 
+  kubernetes:apps/v1:Deployment facility-1-api creating (0s) 
+  kubernetes:apps/v1:Deployment facility-1-sync creating (0s) 
+  kubernetes:apps/v1:Deployment facility-1-tasks creating (0s) 
@ Updating....
+  kubernetes:apps/v1:Deployment facility-1-api creating (0s) 
+  kubernetes:apps/v1:Deployment facility-1-sync creating (0s) 
+  kubernetes:batch/v1:Job facility-2-migrator creating (42s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-2-migrator-9639626f-8zl67]: Container "migrator" completed with exit code 0
+  kubernetes:batch/v1:Job central-migrator creating (42s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-migrator-ffc07834-2z7nz]: Container "migrator" completed with exit code 0
+  kubernetes:apps/v1:Deployment facility-1-tasks creating (0s) 
+  kubernetes:apps/v1:Deployment facility-1-api creating (0s) warning: [MinimumReplicasUnavailable] Deployment does not have minimum availability.
+  kubernetes:apps/v1:Deployment facility-1-api creating (0s) Waiting for app ReplicaSet to be available (0/2 Pods available)
+  kubernetes:apps/v1:Deployment facility-1-sync creating (0s) warning: [MinimumReplicasUnavailable] Deployment does not have minimum availability.
+  kubernetes:apps/v1:Deployment facility-1-sync creating (0s) Waiting for app ReplicaSet to be available (0/1 Pods available)
+  kubernetes:apps/v1:Deployment facility-1-tasks creating (0s) warning: [MinimumReplicasUnavailable] Deployment does not have minimum availability.
@ Updating....
+  kubernetes:apps/v1:Deployment facility-1-tasks creating (1s) Waiting for app ReplicaSet to be available (0/1 Pods available)
@ Updating....
+  kubernetes:batch/v1:Job facility-2-migrator creating (44s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-2-migrator-9639626f" to succeed (Active: 0 | Succeeded: 0 | Failed: 0)
+  kubernetes:batch/v1:Job central-migrator creating (44s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-migrator-ffc07834" to succeed (Active: 0 | Succeeded: 0 | Failed: 0)
+  kubernetes:batch/v1:Job central-migrator creating (44s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-migrator-ffc07834" to succeed (Active: 0 | Succeeded: 1 | Failed: 0)
+  kubernetes:batch/v1:Job central-migrator creating (44s) 
+  kubernetes:batch/v1:Job facility-2-migrator creating (44s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-2-migrator-9639626f" to succeed (Active: 0 | Succeeded: 1 | Failed: 0)
+  kubernetes:batch/v1:Job facility-2-migrator creating (44s) 
+  kubernetes:batch/v1:Job facility-2-migrator created (44s) 
+  kubernetes:batch/v1:Job central-migrator created (45s) 
@ Updating....
+  kubernetes:apps/v1:Deployment facility-2-tasks creating (0s) 
+  kubernetes:apps/v1:Deployment facility-2-api creating (0s) 
+  kubernetes:apps/v1:Deployment facility-2-sync creating (0s) 
+  kubernetes:batch/v1:Job central-provisioner creating (0s) 
+  kubernetes:apps/v1:Deployment facility-2-tasks creating (0s) 
+  kubernetes:apps/v1:Deployment facility-2-api creating (0s) 
+  kubernetes:apps/v1:Deployment facility-1-tasks creating (3s) Deployment initialization complete
+  kubernetes:apps/v1:Deployment facility-1-tasks creating (3s) 
+  kubernetes:apps/v1:Deployment facility-2-sync creating (0s) 
+  kubernetes:apps/v1:Deployment facility-2-api creating (0s) warning: [MinimumReplicasUnavailable] Deployment does not have minimum availability.
@ Updating....
+  kubernetes:apps/v1:Deployment facility-1-tasks created (4s) 
+  kubernetes:batch/v1:Job central-provisioner creating (0s) 
+  kubernetes:apps/v1:Deployment facility-2-tasks creating (1s) warning: [MinimumReplicasUnavailable] Deployment does not have minimum availability.
+  kubernetes:apps/v1:Deployment facility-2-tasks creating (1s) Waiting for app ReplicaSet to be available (0/1 Pods available)
+  kubernetes:apps/v1:Deployment facility-2-api creating (1s) Waiting for app ReplicaSet to be available (0/2 Pods available)
+  kubernetes:apps/v1:Deployment facility-2-sync creating (1s) warning: [MinimumReplicasUnavailable] Deployment does not have minimum availability.
+  kubernetes:apps/v1:Deployment facility-2-sync creating (1s) Waiting for app ReplicaSet to be available (0/1 Pods available)
+  kubernetes:batch/v1:Job central-provisioner creating (1s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-provisioner-8cd6f07e" to start
+  kubernetes:batch/v1:Job central-provisioner creating (1s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-provisioner-8cd6f07e" to succeed (Active: 1 | Succeeded: 0 | Failed: 0)
@ Updating.....
+  kubernetes:apps/v1:Deployment facility-2-tasks creating (3s) Deployment initialization complete
+  kubernetes:apps/v1:Deployment facility-2-tasks creating (3s) 
@ Updating....
+  kubernetes:apps/v1:Deployment facility-2-tasks created (3s) 
@ Updating......
+  kubernetes:apps/v1:Deployment facility-1-api creating (10s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-api-8b65fb46f-cxs5p]: containers with unready status: [server]
+  kubernetes:apps/v1:Deployment facility-1-api creating (10s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-api-8b65fb46f-r7phw]: containers with unready status: [server]
+  kubernetes:apps/v1:Deployment facility-1-sync creating (10s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-sync-754d8c557c-4fr9n]: containers with unready status: [server]
@ Updating......
+  kubernetes:apps/v1:Deployment facility-2-api creating (10s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-2-api-5bccb7f759-hr5bj]: containers with unready status: [server]
+  kubernetes:apps/v1:Deployment facility-2-api creating (10s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-2-api-5bccb7f759-q7bkg]: containers with unready status: [server]
+  kubernetes:apps/v1:Deployment facility-2-sync creating (10s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-2-sync-6585c778dc-6x98n]: containers with unready status: [server]
@ Updating.......................................................................
+  kubernetes:batch/v1:Job central-provisioner creating (78s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-provisioner-8cd6f07e-7bjjs]: Container "provisioner" completed with exit code 0
@ Updating.....
+  kubernetes:batch/v1:Job central-provisioner creating (80s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-provisioner-8cd6f07e" to succeed (Active: 0 | Succeeded: 0 | Failed: 0)
+  kubernetes:batch/v1:Job central-provisioner creating (80s) Waiting for Job "tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-provisioner-8cd6f07e" to succeed (Active: 0 | Succeeded: 1 | Failed: 0)
+  kubernetes:batch/v1:Job central-provisioner creating (80s) 
@ Updating....
+  kubernetes:batch/v1:Job central-provisioner created (81s) 
+  kubernetes:apps/v1:Deployment central-fhir-resolver creating (0s) 
+  kubernetes:apps/v1:Deployment central-api creating (0s) 
+  kubernetes:apps/v1:Deployment central-tasks creating (0s) 
+  kubernetes:apps/v1:Deployment central-fhir-refresh creating (0s) 
+  kubernetes:apps/v1:Deployment central-fhir-resolver creating (0s) 
+  kubernetes:apps/v1:Deployment central-api creating (0s) 
@ Updating....
+  kubernetes:apps/v1:Deployment central-tasks creating (0s) 
+  kubernetes:apps/v1:Deployment central-fhir-refresh creating (0s) 
+  kubernetes:apps/v1:Deployment central-fhir-resolver creating (1s) warning: [MinimumReplicasUnavailable] Deployment does not have minimum availability.
+  kubernetes:apps/v1:Deployment central-tasks creating (1s) warning: [MinimumReplicasUnavailable] Deployment does not have minimum availability.
+  kubernetes:apps/v1:Deployment central-fhir-resolver creating (1s) Waiting for app ReplicaSet to be available (0/1 Pods available)
@ Updating....
+  kubernetes:apps/v1:Deployment central-fhir-refresh creating (1s) warning: [MinimumReplicasUnavailable] Deployment does not have minimum availability.
+  kubernetes:apps/v1:Deployment central-fhir-refresh creating (2s) Waiting for app ReplicaSet to be available (0/1 Pods available)
+  kubernetes:apps/v1:Deployment central-api creating (2s) warning: [MinimumReplicasUnavailable] Deployment does not have minimum availability.
+  kubernetes:apps/v1:Deployment central-api creating (2s) Waiting for app ReplicaSet to be available (0/2 Pods available)
+  kubernetes:apps/v1:Deployment central-fhir-resolver creating (2s) Deployment initialization complete
+  kubernetes:apps/v1:Deployment central-fhir-resolver creating (2s) 
+  kubernetes:apps/v1:Deployment central-fhir-resolver created (2s) 
@ Updating.......
+  kubernetes:apps/v1:Deployment facility-1-api creating (90s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-api-8b65fb46f-r7phw]: containers with unready status: [server]Container "server" terminated at 2025-04-17T14:10:43Z (Error: exit code 1)
@ Updating....
+  kubernetes:apps/v1:Deployment central-tasks creating (7s) Waiting for app ReplicaSet to be available (0/1 Pods available)
@ Updating....
+  kubernetes:apps/v1:Deployment central-tasks creating (8s) Deployment initialization complete
+  kubernetes:apps/v1:Deployment central-tasks creating (8s) 
+  kubernetes:apps/v1:Deployment central-tasks created (8s) 
@ Updating....
+  kubernetes:apps/v1:Deployment facility-2-api creating (90s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-2-api-5bccb7f759-q7bkg]: containers with unready status: [server]Container "server" terminated at 2025-04-17T14:10:46Z (Error: exit code 1)
@ Updating....
+  kubernetes:apps/v1:Deployment central-api creating (10s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-api-12bedad6-5776b868d8-9pb88]: containers with unready status: [server]
+  kubernetes:apps/v1:Deployment central-api creating (10s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/central-api-12bedad6-5776b868d8-pr46l]: containers with unready status: [server]
@ Updating.........
+  kubernetes:apps/v1:Deployment facility-1-sync creating (100s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-sync-754d8c557c-4fr9n]: containers with unready status: [server]Container "server" terminated at 2025-04-17T14:10:53Z (Error: exit code 137)
@ Updating......
+  kubernetes:apps/v1:Deployment central-fhir-refresh creating (19s) Deployment initialization complete
+  kubernetes:apps/v1:Deployment central-fhir-refresh creating (19s) 
@ Updating....
+  kubernetes:apps/v1:Deployment central-fhir-refresh created (20s) 
+  kubernetes:apps/v1:Deployment central-api creating (20s) Waiting for app ReplicaSet to be available (1/2 Pods available)
@ Updating....
+  kubernetes:apps/v1:Deployment central-api creating (20s) Deployment initialization complete
+  kubernetes:apps/v1:Deployment central-api creating (20s) 
+  kubernetes:apps/v1:Deployment central-api created (21s) 
@ Updating........
+  kubernetes:apps/v1:Deployment facility-1-api creating (110s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-api-8b65fb46f-r7phw]: containers with unready status: [server]Container "server" completed with exit code 1
+  kubernetes:apps/v1:Deployment facility-1-api creating (110s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-1-api-8b65fb46f-cxs5p]: containers with unready status: [server]Container "server" terminated at 2025-04-17T14:11:03Z (Error: exit code 137)
@ Updating....
+  kubernetes:apps/v1:Deployment facility-1-api creating (111s) Waiting for app ReplicaSet to be available (1/2 Pods available)
@ Updating.....
+  kubernetes:apps/v1:Deployment facility-2-api creating (110s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-2-api-5bccb7f759-q7bkg]: containers with unready status: [server]Container "server" completed with exit code 1
+  kubernetes:apps/v1:Deployment facility-2-api creating (110s) warning: [Pod tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a/facility-2-api-5bccb7f759-hr5bj]: containers with unready status: [server]Container "server" completed with exit code 1
+  kubernetes:apps/v1:Deployment facility-1-api creating (113s) Deployment initialization complete
+  kubernetes:apps/v1:Deployment facility-1-api creating (113s) 
@ Updating....
+  kubernetes:apps/v1:Deployment facility-1-api created (114s) 
+  kubernetes:apps/v1:Deployment facility-1-sync creating (114s) Deployment initialization complete
+  kubernetes:apps/v1:Deployment facility-1-sync creating (114s) 
+  kubernetes:apps/v1:Deployment facility-1-sync created (114s) 
@ Updating......
+  kubernetes:apps/v1:Deployment facility-2-api creating (113s) Waiting for app ReplicaSet to be available (1/2 Pods available)
@ Updating....
+  kubernetes:apps/v1:Deployment facility-2-api creating (114s) Deployment initialization complete
+  kubernetes:apps/v1:Deployment facility-2-api creating (114s) 
+  kubernetes:apps/v1:Deployment facility-2-sync creating (115s) Deployment initialization complete
+  kubernetes:apps/v1:Deployment facility-2-sync creating (115s) 
+  kubernetes:apps/v1:Deployment facility-2-api created (115s) 
+  kubernetes:apps/v1:Deployment facility-2-sync created (115s) 
+  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a created (323s) 16 messages
Diagnostics:
 pulumi:pulumi:Stack (tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a):
   Waiting for central-db...
   Waiting for facility-1-db...
   Waiting for facility-2-db...

   Using tailscale proxy https://k8s-operator-tamanu-internal-main.tail53aef.ts.net

   Secret central-db-superuser not found or not ready: Error: HTTP-Code: 404
   Message: Unknown API Status Code!
   Body: "{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"secrets \\\"central-db-superuser\\\" not found\",\"reason\":\"NotFound\",\"details\":{\"name\":\"central-db-superuser\",\"kind\":\"secrets\"},\"code\":404}
"
   Headers: {"audit-id":"ecdd088d-cda7-48ee-b859-377eae70e67e","cache-control":"no-cache, private","connection":"close","content-length":"214","content-type":"application/json","date":"Thu, 17 Apr 2025 14:08:39 GMT","x-kubernetes-pf-flowschema-uid":"7d46608e-fb55-48cf-90aa-981c856b1d54","x-kubernetes-pf-prioritylevel-uid":"ed197318-138c-44ea-825e-1d4df495928e"}
   Secret facility-1-db-superuser not found or not ready: Error: HTTP-Code: 404
   Message: Unknown API Status Code!
   Body: "{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"secrets \\\"facility-1-db-superuser\\\" not found\",\"reason\":\"NotFound\",\"details\":{\"name\":\"facility-1-db-superuser\",\"kind\":\"secrets\"},\"code\":404}
"
   Headers: {"audit-id":"7504af11-6396-498d-a04f-22b00a24d476","cache-control":"no-cache, private","connection":"close","content-length":"220","content-type":"application/json","date":"Thu, 17 Apr 2025 14:08:39 GMT","x-kubernetes-pf-flowschema-uid":"7d46608e-fb55-48cf-90aa-981c856b1d54","x-kubernetes-pf-prioritylevel-uid":"ed197318-138c-44ea-825e-1d4df495928e"}
   Secret facility-2-db-superuser not found or not ready: Error: HTTP-Code: 404
   Message: Unknown API Status Code!
   Body: "{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"secrets \\\"facility-2-db-superuser\\\" not found\",\"reason\":\"NotFound\",\"details\":{\"name\":\"facility-2-db-superuser\",\"kind\":\"secrets\"},\"code\":404}
"
   Headers: {"audit-id":"7e007abe-7ed9-429d-8914-da820488d5d9","cache-control":"no-cache, private","connection":"close","content-length":"220","content-type":"application/json","date":"Thu, 17 Apr 2025 14:08:39 GMT","x-kubernetes-pf-flowschema-uid":"7d46608e-fb55-48cf-90aa-981c856b1d54","x-kubernetes-pf-prioritylevel-uid":"ed197318-138c-44ea-825e-1d4df495928e"}

Outputs:
   urls: {
       Central    : "https://central.feature-epi-1128-build-workflow-to-record-a-medication-a.cd.tamanu.app"
       Facility- 1: "https://facility-1.feature-epi-1128-build-workflow-to-record-a-medication-a.cd.tamanu.app"
       Facility- 2: "https://facility-2.feature-epi-1128-build-workflow-to-record-a-medication-a.cd.tamanu.app"
   }

Resources:
   + 53 created

Duration: 5m24s

   </pre>
   
   </details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-17T14:13:42Z
body: #### :tropical_drink: `destroy` on tamanu-on-k8s/bes/tamanu-on-k8s/feature-epi-1128-build-workflow-to-record-a-medication-a

   <details>
   <summary>Pulumi report</summary>

   
   <pre>
   Destroying (bes/feature-epi-1128-build-workflow-to-record-a-medication-a)

View Live: https://app.pulumi.com/bes/tamanu-on-k8s/feature-epi-1128-build-workflow-to-record-a-medication-a/updates/2

@ Destroying......

-  kubernetes:apps/v1:Deployment central-api deleting (0s) 
-  kubernetes:apps/v1:Deployment central-fhir-refresh deleting (0s) 
-  kubernetes:apps/v1:Deployment facility-1-api deleting (0s) 
-  kubernetes:apps/v1:Deployment central-fhir-resolver deleting (0s) 
-  kubernetes:apps/v1:Deployment facility-1-tasks deleting (0s) 
-  kubernetes:apps/v1:Deployment central-tasks deleting (0s) 
-  kubernetes:apps/v1:Deployment facility-2-sync deleting (0s) 
-  kubernetes:apps/v1:Deployment facility-2-tasks deleting (0s) 
-  kubernetes:apps/v1:Deployment facility-2-api deleting (0s) 
-  kubernetes:apps/v1:Deployment facility-1-sync deleting (0s) 
@ Destroying......
-  kubernetes:apps/v1:Deployment facility-1-sync deleting (2s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment central-api deleting (3s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment facility-1-tasks deleting (3s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment facility-2-tasks deleting (3s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment facility-2-sync deleting (3s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment facility-1-api deleting (3s) Resource scheduled for deletion
@ Destroying....
-  kubernetes:apps/v1:Deployment central-fhir-refresh deleting (3s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment facility-2-api deleting (3s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment central-tasks deleting (3s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment central-fhir-resolver deleting (3s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment central-tasks deleting (4s) 
-  kubernetes:apps/v1:Deployment central-tasks deleted (4s) 
-  kubernetes:apps/v1:Deployment facility-2-sync deleting (4s) 
-  kubernetes:apps/v1:Deployment central-fhir-resolver deleting (4s) 
-  kubernetes:apps/v1:Deployment facility-2-sync deleted (4s) 
-  kubernetes:apps/v1:Deployment central-fhir-resolver deleted (4s) 
-  kubernetes:apps/v1:Deployment facility-1-sync deleting (4s) 
-  kubernetes:apps/v1:Deployment facility-2-tasks deleting (4s) 
-  kubernetes:apps/v1:Deployment facility-1-sync deleted (4s) 
-  kubernetes:apps/v1:Deployment facility-2-tasks deleted (4s) 
-  kubernetes:apps/v1:Deployment central-fhir-refresh deleting (4s) 
-  kubernetes:apps/v1:Deployment central-fhir-refresh deleted (4s) 
@ Destroying....
-  kubernetes:apps/v1:Deployment facility-1-tasks deleting (4s) 
-  kubernetes:apps/v1:Deployment facility-1-tasks deleted (4s) 
@ Destroying................................
-  kubernetes:apps/v1:Deployment central-api deleting (33s) 
-  kubernetes:apps/v1:Deployment central-api deleted (33s) 
-  kubernetes:apps/v1:Deployment facility-1-api deleting (33s) 
-  kubernetes:apps/v1:Deployment facility-1-api deleted (33s) 
-  kubernetes:apps/v1:Deployment facility-2-api deleting (34s) 
-  kubernetes:apps/v1:Deployment facility-2-api deleted (34s) 
-  kubernetes:networking.k8s.io/v1:Ingress facility-2 deleting (0s) 
-  kubernetes:networking.k8s.io/v1:Ingress central deleting (0s) 
-  kubernetes:networking.k8s.io/v1:Ingress facility-1 deleting (0s) 
-  kubernetes:batch/v1:Job facility-2-migrator deleting (0s) 
-  kubernetes:batch/v1:Job central-provisioner deleting (0s) 
-  kubernetes:batch/v1:Job facility-1-migrator deleting (0s) 
@ Destroying....
-  kubernetes:networking.k8s.io/v1:Ingress facility-2 deleting (1s) 
-  kubernetes:networking.k8s.io/v1:Ingress facility-2 deleted (1s) 
-  kubernetes:networking.k8s.io/v1:Ingress central deleting (1s) 
-  kubernetes:networking.k8s.io/v1:Ingress central deleted (1s) 
@ Destroying....
-  kubernetes:batch/v1:Job facility-2-migrator deleting (1s) 
-  kubernetes:batch/v1:Job facility-2-migrator deleted (1s) 
-  kubernetes:networking.k8s.io/v1:Ingress facility-1 deleting (1s) 
-  kubernetes:networking.k8s.io/v1:Ingress facility-1 deleted (1s) 
-  kubernetes:batch/v1:Job central-provisioner deleting (1s) 
-  kubernetes:batch/v1:Job central-provisioner deleted (1s) 
-  kubernetes:batch/v1:Job facility-1-migrator deleting (1s) 
-  kubernetes:batch/v1:Job facility-1-migrator deleted (1s) 
-  kubernetes:core/v1:Service central-web deleting (0s) 
-  kubernetes:core/v1:Service facility-1-web deleting (0s) 
-  kubernetes:apps/v1:Deployment facility-2-web deleting (0s) 
-  kubernetes:batch/v1:Job central-migrator deleting (0s) 
-  kubernetes:apps/v1:Deployment central-web deleting (0s) 
-  kubernetes:core/v1:Service facility-2-web deleting (0s) 
-  kubernetes:apps/v1:Deployment facility-1-web deleting (0s) 
@ Destroying....
-  kubernetes:core/v1:Service central-web deleting (0s) 
-  kubernetes:core/v1:Service central-web deleted (0.53s) 
-  kubernetes:core/v1:Service facility-1-web deleting (0s) 
-  kubernetes:core/v1:Service facility-1-web deleted (0.96s) 
@ Destroying....
-  kubernetes:batch/v1:Job central-migrator deleting (1s) 
-  kubernetes:batch/v1:Job central-migrator deleted (1s) 
-  kubernetes:apps/v1:Deployment central-web deleting (1s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment facility-2-web deleting (1s) Resource scheduled for deletion
-  kubernetes:core/v1:Service facility-2-web deleting (1s) 
-  kubernetes:core/v1:Service facility-2-web deleted (1s) 
-  kubernetes:apps/v1:Deployment facility-1-web deleting (1s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment facility-2-web deleting (1s) 
-  kubernetes:apps/v1:Deployment facility-2-web deleted (1s) 
@ Destroying....
-  kubernetes:apps/v1:Deployment central-web deleting (2s) 
-  kubernetes:apps/v1:Deployment central-web deleted (2s) 
-  kubernetes:apps/v1:Deployment facility-1-web deleting (2s) 
-  kubernetes:apps/v1:Deployment facility-1-web deleted (2s) 
-  kubernetes:core/v1:Secret bugsnag deleting (0s) 
-  kubernetes:core/v1:ServiceAccount app-sa deleting (0s) 
-  kubernetes:core/v1:Secret facility-1-db-url deleting (0s) 
-  kubernetes:core/v1:Service facility-1-api deleting (0s) 
-  kubernetes:core/v1:ConfigMap central deleting (0s) 
-  kubernetes:core/v1:ConfigMap facility-1 deleting (0s) 
-  kubernetes:core/v1:Service central-api deleting (0s) 
-  kubernetes:core/v1:Service facility-1-sync deleting (0s) 
-  bes:tamanu:WebFrontend facility-1 deleting (0s) 
-  kubernetes:hnc.x-k8s.io/v1alpha2:HierarchyConfiguration parent-ns deleting (0s) 
-  kubernetes:core/v1:Secret facility-2-db-url deleting (0s) 
-  kubernetes:core/v1:Service facility-2-api deleting (0s) 
-  kubernetes:core/v1:Secret mailgun deleting (0s) 
-  bes:tamanu:WebFrontend facility-2 deleting (0s) 
-  kubernetes:core/v1:ConfigMap provisioning deleting (0s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-2-db deleting (0s) 
@ Destroying....
-  kubernetes:core/v1:Secret bugsnag deleting (0s) 
-  kubernetes:core/v1:Secret bugsnag deleted (0.68s) 
@ Destroying....
-  kubernetes:core/v1:ServiceAccount app-sa deleting (1s) 
-  kubernetes:core/v1:ServiceAccount app-sa deleted (1s) 
-  kubernetes:core/v1:Secret facility-1-db-url deleting (1s) 
-  kubernetes:core/v1:Secret facility-1-db-url deleted (1s) 
-  kubernetes:core/v1:Service facility-1-api deleting (1s) 
-  kubernetes:core/v1:Service facility-1-api deleted (1s) 
-  kubernetes:core/v1:ConfigMap central deleting (1s) 
-  kubernetes:core/v1:ConfigMap central deleted (1s) 
-  kubernetes:core/v1:ConfigMap facility-1 deleting (1s) 
-  kubernetes:core/v1:ConfigMap facility-1 deleted (1s) 
-  kubernetes:core/v1:Service central-api deleting (1s) 
-  kubernetes:core/v1:Service central-api deleted (1s) 
@ Destroying....
-  kubernetes:core/v1:Service facility-1-sync deleting (2s) 
-  kubernetes:core/v1:Service facility-1-sync deleted (2s) 
-  kubernetes:hnc.x-k8s.io/v1alpha2:HierarchyConfiguration parent-ns deleting (2s) 
-  kubernetes:hnc.x-k8s.io/v1alpha2:HierarchyConfiguration parent-ns deleted (2s) 
-  kubernetes:core/v1:Secret facility-2-db-url deleting (2s) 
-  kubernetes:core/v1:Secret facility-2-db-url deleted (2s) 
-  kubernetes:core/v1:Service facility-2-api deleting (2s) 
-  kubernetes:core/v1:Service facility-2-api deleted (2s) 
-  kubernetes:core/v1:Secret mailgun deleting (3s) 
-  kubernetes:core/v1:Secret mailgun deleted (3s) 
@ Destroying....
-  kubernetes:core/v1:ConfigMap provisioning deleting (3s) 
-  kubernetes:core/v1:ConfigMap provisioning deleted (3s) 
-  bes:tamanu:WebFrontend central deleting (0s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster central-db deleting (0s) 
-  kubernetes:core/v1:ConfigMap facility-2 deleting (0s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-2-db deleting (3s) Resource scheduled for deletion
-  kubernetes:core/v1:Service facility-2-sync deleting (0s) 
-  kubernetes:networking.k8s.io/v1:Ingress bare-domain deleting (0s) 
@ Destroying....
-  kubernetes:core/v1:Secret pullsecret-github deleting (0s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-2-db deleting (4s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-2-db deleted (4s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-1-db deleting (0s) 
-  kubernetes:core/v1:Secret tupaia deleting (0s) 
-  kubernetes:core/v1:Secret central-db-url deleting (0s) 
@ Destroying.....
-  kubernetes:core/v1:ConfigMap facility-2 deleting (2s) 
-  kubernetes:core/v1:ConfigMap facility-2 deleted (2s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster central-db deleting (3s) Resource scheduled for deletion
-  kubernetes:networking.k8s.io/v1:Ingress bare-domain deleting (2s) 
-  kubernetes:networking.k8s.io/v1:Ingress bare-domain deleted (2s) 
-  kubernetes:core/v1:Service facility-2-sync deleting (2s) 
-  kubernetes:core/v1:Service facility-2-sync deleted (2s) 
-  kubernetes:core/v1:Secret pullsecret-github deleting (2s) 
-  kubernetes:core/v1:Secret pullsecret-github deleted (2s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-1-db deleting (2s) Resource scheduled for deletion
-  kubernetes:core/v1:Secret tupaia deleting (2s) 
-  kubernetes:core/v1:Secret tupaia deleted (2s) 
-  kubernetes:core/v1:Secret central-db-url deleting (2s) 
-  kubernetes:core/v1:Secret central-db-url deleted (2s) 
@ Destroying....
-  kubernetes:postgresql.cnpg.io/v1:Cluster central-db deleting (3s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster central-db deleted (3s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-1-db deleting (3s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-1-db deleted (3s) 
@ Destroying....
-  bes:tamanu:CentralServer central deleting (0s) 
-  bes:tamanu:FacilityServer 2 deleting (0s) 
-  bes:tamanu:FacilityServer 1 deleting (0s) 
-  kubernetes:core/v1:Namespace tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a deleting (0s) 
@ Destroying....
-  kubernetes:core/v1:Namespace tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a deleting (1s) Resource scheduled for deletion
@ Destroying.........
-  kubernetes:core/v1:Namespace tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a deleting (7s) 
-  kubernetes:core/v1:Namespace tamanu-feature-epi-1128-build-workflow-to-record-a-medication-a deleted (7s) 
-  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a deleting (0s) 
@ Destroying....
-  pulumi:pulumi:Stack tamanu-on-k8s-feature-epi-1128-build-workflow-to-record-a-medication-a deleted (0.39s) 
Outputs:
 - urls: {
     - Central    : "https://central.feature-epi-1128-build-workflow-to-record-a-medication-a.cd.tamanu.app"
     - Facility- 1: "https://facility-1.feature-epi-1128-build-workflow-to-record-a-medication-a.cd.tamanu.app"
     - Facility- 2: "https://facility-2.feature-epi-1128-build-workflow-to-record-a-medication-a.cd.tamanu.app"
   }

Resources:
   - 53 deleted

Duration: 56s

The resources in the stack have been deleted, but the history and configuration associated with the stack are still maintained. 
If you want to remove the stack completely, run `pulumi stack rm bes/feature-epi-1128-build-workflow-to-record-a-medication-a`.
   </pre>
   
   </details>
author_association: NONE