Module Evidence Annex — QMS Nordic
| Document ID | QMSN-MEA-001 |
| Applies to | QMS Nordic v1.5.0 |
| Generated | from the source of truth, not written by hand |
Generated from the platform's requirement register, not written by hand, and regenerated on every change. A build check fails if this document falls out of step with the code it describes, so it cannot quietly stop being true — which is what happens to a hand-maintained matrix within two releases.
The requirement register behind the URS and the traceability matrix is deliberately narrow: the 26 properties a regulator holds any electronic quality-record system to, whatever a customer runs in it. A customer who also runs CAPA, complaints, training or suppliers in the platform must validate those for their intended use (ISO 13485 §4.1.6) — and may lean on the supplier's testing to do less of their own.
This annex is that testing, module by module, held to the same discipline as the register: which suites, what they establish, and, at more length, what they leave to you. The test names are read from the suites themselves when this document is generated, so a renamed test appears here and a deleted suite fails the build. The Performance Qualification (QMSN-PQ-001) cites these sections so your own steps stop where ours end.
These are not requirements and do not appear in the matrix. A requirement is a promise held to full evidence on every release. Module evidence is a description of what we test, allowed to be partial because it says where it is partial.
| Module | Id | Suites | Tests |
|---|---|---|---|
| Product classification | M-CLS | classifier, rules-engine | 51 |
| Corrective and preventive action | M-CAPA | capa-gates, complaint-capa-link, complaint-capa-link, same-lot, same-lot | 106 |
| Complaints and vigilance | M-CMP | complaint-gates, reportability, regulatory-citations, reportability-determination, reportability-determination, survey-intake, complaint-vocabulary, ticket-complaint, trend-detection | 117 |
| Management review | M-MR | management-review-gates, review-actions, review-actions-gates | 38 |
| Post-market surveillance — proactive feedback | M-PMS | pms-feedback-selection, pms-feedback-screen, pms-feedback-unsubscribe, pms-feedback-escalation, pms-feedback-delivery, pms-feedback-psur, pms-feedback-plan-text, pms-feedback-gates, pms-feedback-i18n, pms-feedback-customisation, pms-feedback-retention, pms-report-due, pms-feedback-reply, pms-feedback-closure, pms-feedback-end-users, sending-domain | 298 |
| Servicing — analysed for complaints | M-SVC | servicing-complaint, servicing-gates | 30 |
| Supplier control | M-SUP | supplier-gates, supplier-questionnaire-answers | 14 |
| Training and competence | M-TRN | training-gates | 8 |
| Equipment and calibration | M-EQP | equipment-gates, calibration-gates | 9 |
| Software lifecycle (IEC 62304) | M-SW | software-gates | 9 |
| Usability engineering (IEC 62366-1) | M-USE | lifecycle-gates | 4 |
| Process and software validation runs | M-VAL | software-validation, lifecycle-gates, revalidation-drift | 30 |
| Regulatory submissions | M-SUB | submission-gates | 10 |
| Traceability of units leaving the building | M-TRACE | traceability-rules, the-carrier-is-a-carrier, register-recall-dispatch-labels, recall-chain, the-product-says-whether-it-is-sterile, the-product-says-whether-it-is-sterile, the-label-prints-as-issued, the-label-prints-as-issued, customer-identity, a-shipment-goes-to-a-customer, a-shipment-before-the-market-says-why, a-shipment-before-the-market-says-why, customers, importers-and-distributors, importers-and-distributors | 301 |
| Product register and market release | M-PROD | product-udi, product-release-gates | 14 |
M-CLS — Product classification
Your obligation: EU MDR Annex VIII · EU MDR Article 51 · ISO 13485 §7.1
What our tests establish. For known inputs the classifier returns the expected class under the EU MDR and FDA rules, cites the rule it fired, and the rule pack derives the obligations that follow from a class.
classifier.test.ts:
- non-invasive → Class I baseline
- implantable in contact with heart → Class III
- SaMD whose decisions may cause death or irreversible deterioration → Class III
- a measuring function makes a Class I device Im, and does not raise it
- a nerve stimulator (active, not life-supporting) is not a settled Class I
- an active measuring device is weighed under Rule 10, not raised by its measuring function
- standalone software is a floor of IIa with the rest of Rule 11 unevaluated
- a non-invasive device names Rules 2 to 4, which can raise it
- a rule is not named when it cannot raise the class returned
- every unevaluated rule asks a question a person can answer
- a ${duration.toLowerCase().replace(
- non-invasive non-software → Class I
- software-driven → Class II
- implantable → Class III
- raises a long-term surgically invasive device in contact with the CNS to Class III
- treats an implantable the same way, because Rule 8 does
- says which exceptions it did not evaluate, rather than presenting a floor as an answer
- claims nothing it evaluated when the exception is settled
- does not put a short-term surgically invasive device under Rule 8
- is honest that an FDA result is not a classification
- does not open an implant's FDA rationale with the Class I baseline (NV-045)
- says nothing placed a device rather than borrowing Class I as a finding
- stores the unevaluated rules beside the class they qualify
- names each rule by its clause in words, never by the engine's identifier
- calls a provisional class unsettled and possibly too low, and says a few rules could lower it
- does not call a confirmed or a superseded class unsettled, and still lists what was open
- says a class an earlier engine decided is not settled, though it stored no rules
- heads a class chosen by hand as chosen by hand, not as possibly too low
- is what the product page renders for each classification
- does not open an implant's rationale with the rule for non-invasive devices
- still cites Rule 1 for the devices Rule 1 is about, under either spelling
- places a device that goes through an orifice under Rule 5, with what is still to ask
- records a rule only when that rule moved the class
- a device that touches nothing is asked Rules 2 to 4, as a skin-contact one is
- a transient surgically invasive device is asked Rule 6's limbs, and not Rule 7's
- a short-term one is asked Rule 7's, and not Rule 6's
- Rule 6's limbs reach IIb, so a transient device already at IIb is not asked them
- Rule 7's biological-effect limb reaches III: asked at IIb, not at III
- asks the special rules (14 to 21) of every device below Class III, each rule on its own
- asks Rule 16 as the rule reads: contact-lens care, and disinfecting or sterilising devices — not physical cleaning
- does not ask the special rules of a Class III device, which they cannot raise
- does not call a Class III body-orifice device provisional for Rule 5, which reaches IIa
- does not call a Class III implant provisional for Rule 8's limbs, which reach III
- puts the rule that placed a body-orifice device before the Art. 52 note a measuring function adds
- does not say so of a device a rule did place
- names every unevaluated rule's clause in words, for every row ever written
rules-engine.test.ts:
- regulator predicate matches when classification is present
- classMin enforces ordering
- all/any/not compose correctly
- EU MDR Class IIb implantable triggers SSCP and Implant Card
- FDA-only context does not pull MDR-only obligations
What they do not cover — and so what your PQ must. Whether your device's inputs are the right inputs is a regulatory judgement the platform records but does not make. The pack pins the rule version used; it does not validate your rationale, and it does not replace the classification in your technical documentation.
M-CAPA — Corrective and preventive action
Your obligation: ISO 13485 §8.5.2 · ISO 13485 §8.5.3
What our tests establish. Walking one case through the real server actions on a real database: it cannot move to implementation until both a corrective and a preventive action are written; effectiveness cannot be verified before implementation; it cannot be closed without a verified effectiveness check, and the refusal names §8.5.2(f); every transition is on the audit trail with the previous state. A case can address several complaints and a complaint can lead to several cases, and every link states which cause it addresses — a link without a reason is refused. A closed case refuses a new cause: when a cause recurs after its effectiveness was verified, a new case is opened that follows it and must state what it will do differently, while the closed case is left unmodified and shows the recurrence through a marker derived from the case that followed. A complaint about a unit made before the fix may be attached to the closed case, and is recorded as not counting against its effectiveness. A link cannot be removed from a closed case, and a removal from an open one keeps the link's content on the audit trail. A case opened from a complaint is linked to it in the same transaction, so a case can never exist without its record of what caused it. Links do not cross organisations. A complaint on the same product and lot as a complaint a case addressed — the lot compared without regard to letter case or surrounding spaces — is shown on both records: on the complaint, with the cases behind the other complaints on that lot and, where one closed as effective before this complaint became known, that this may be a recurrence after the fix, with the two routes above; on the case, as a complaint on one of its lots that is not linked to it. The owner of the closed case is told when such a complaint is recorded.
capa-gates.integration.test.ts (against a real database, as a signed-in user):
- opens a case with a number and an audit record, then shows it
- will not move to implementation until both actions are written
- refuses the outcome of a monitoring period that has not run out
- will not be verified effective while the monitoring its plan declared is still running (NV-081)
- records the monitoring outcome once the period has run out
- cannot be closed without a verdict on effectiveness, and says which clause
- asks the planner why, before letting them verify their own actions (NV-082)
- and records the reason when they give one
- closes on the same person's verification, because no rule forbids it
- refuses self-verification where the organisation's own policy requires separation
- does not verify effectiveness on a case that is not being implemented
- refuses 'effective' when the monitoring period could not tell
- records 'not effective', and closing asks what the follow-up will do differently
- closes it as not effective and opens the follow-up, linked, in one step
- refuses to close it again, and opens no second follow-up
- refuses to rewrite the closed case's actions or investigation, and keeps them
- refuses a second follow-up opened by typing the new-case address
- refuses a monitoring outcome on the closed case
complaint-capa-link.test.ts:
- allows a reasoned link to an open case
- requires a reason, because a link without one is just a list
- does not count whitespace as a reason
- refuses a duplicate, naming both records
- refuses an unknown kind
- never lets a person record a link as carried over
- refuses to attach a cause to a closed case
- tells the user both honest alternatives
- allows a pre-fix unit to attach to the closed case
- allows a pre-fix unit while the case is verifying
- refuses 'pre-dates the fix' when there is no fix yet
- is allowed on an open case, with a reason
- is refused on a closed case, because it closed on that evidence
- requires a reason, which is recorded with the removal
- is allowed against a closed case, with a real rationale
- is refused against an open case — that is just more evidence for it
- requires saying what will be different
- is absent when nothing followed
- names the case that followed
- says the closed case itself is unchanged
- lists several in the order they were opened
- says a case closed as not effective was found not to work, not that it recurred
- from the verdict of the case it follows
- a manual follow-up of a not-effective case is not told the verification was not enough
- counts causes and carried-over links
- does not count a unit that pre-dates the fix
- is refused once its follow-up exists, naming it
- does not apply to a case verified effective, which may be followed again if the cause recurs
complaint-capa-link.integration.test.ts (against a real database, as a signed-in user):
- links several complaints to one case, each with its reason
- links one complaint to several cases — one investigation, two causes
- refuses a link without a reason
- refuses a duplicate link
- never lets a person record a link as carried over
- refuses a new cause on a closed case
- accepts a pre-fix unit on the closed case
- refuses to remove a link from a closed case
- removes a link from an open case and keeps what it said in the trail
- does not link to another organisation's case
- refuses without saying what will be different
- refuses to follow a case that is still open
- opens the follow-up, linked to its complaint in the same transaction, and leaves the closed case alone
same-lot.test.ts:
- trims and ignores case, and nothing more
- needs the same product as well as the same lot
- groups by product and lot
- closed case, complaint known after it closed: a possible recurrence after the fix
- closed case, complaint known before it closed: known before closure, offered as pre-fix
- the day it closed counts as after: nobody can show it was in front of whoever closed it
- open case: more evidence for that case, whatever the dates
- a case closed as not effective, judged where nothing follows it, is its own judgement
- a case closed before verdicts were recorded is taken as effective, as its closure asserted
- a closed case with no closure date is not given the benefit of the doubt
- a complaint already linked to the case has nothing left to decide
- a closed case asks for a decision; a pre-fix link is offered only after a fix taken as effective
- a complaint linked to another case in the line is decided there, with nothing to decide here
- a recurrence is said plainly, with both of the product's own routes
- the latest case of a line says so, and never sends the reader to a case that cannot take the complaint
- a case closed as not effective with nothing following it asks for a case that follows
- an open case asks for a link as a cause, not a new case
- no stored code reaches the reader
- the follow-up address says what the case closed as
- the CAPA page shows the lots of the case's line even when no complaint is linked to the case itself
- the follow-up address is the existing form, from this complaint, with a reason long enough to link
- the notice and the page tell the same story
- is a switchable kind, inbox by default and not emailed unless asked
- has a row on the settings page and is accepted by the save action
- is sent wherever a complaint is recorded with a lot
- tells the owner where a line asks for a decision — and nobody about the others
- falls back to whoever closed it, then to QA and RA, never to the person who recorded it
- says nothing when the complaint carries no lot
- never fails the complaint it was called for
- is the case at the top and every case following it, siblings included, and no other line
- ends on a cycle written by hand
- stands at its open case, or else at the case closed last — which nothing follows
- the line's own first complaint is not a recurrence of its follow-up
- a later complaint is judged against the case closed last, which can be followed
- a not-effective case whose follow-up closed too: the recurrence is surfaced against the follow-up
- while a case in the line is open, a complaint is more evidence for it — not for the closed ones
- a complaint taken by a second case following the first has nothing left to decide
same-lot.integration.test.ts (against a real database, as a signed-in user):
- while the case is open, the other complaint on its lot is more evidence for it
- closes the case as verified effective through the real action
- records a third complaint on the same lot, and the owner of the closed case is told
- the complaint page's data shows the other two complaints and the closed case as a possible recurrence
- the CAPA page's data lists both unlinked complaints, the later one as a possible recurrence
- once linked as a pre-fix recurrence, the complaint page has nothing left to decide
- once a case that follows is opened from the complaint, both pages leave nothing to decide
- after the follow-up closes too, a later complaint is judged against it, and a case following it takes it
- look-alike lots on a busy product do not push the lot's own complaints out
- the follow-up a not-effective closure opens sees a later complaint on its line's lot
What they do not cover — and so what your PQ must. Root-cause method, the adequacy of the actions, your escalation and due-date rules, and what counts as an effective outcome are yours. The platform enforces that the steps happen in order and are recorded; it cannot judge that they were done well. Whether several complaints genuinely share a cause is your investigator's judgement: the platform requires that the cause be stated for each link, not that the statement is right. Whether a unit pre-dates a fix is established from your own production and traceability records; the platform records the classification you make and what it means for the case, but does not check it against them. The platform surfaces complaints on the same lot; whether two lot numbers written differently name one lot, and whether a complaint on the same lot shares the cause, are yours to decide.
M-CMP — Complaints and vigilance
Your obligation: ISO 13485 §8.2.2 · EU MDR Article 87 · EU MDR Article 88 · 21 CFR Part 803
What our tests establish. A complaint flagged as reportable under the EU MDR or to the FDA cannot be closed until the report is recorded with its reference, or a determination is recorded that it is not reportable after all; a report cannot be recorded against a complaint that is not flagged. The reporting clocks — 2, 10 and 15 days under Article 87, 5 and 30 days under Part 803 — run from the awareness date, not from the day the complaint was entered. Harm reported without an explicit serious-injury or malfunction flag is presumed reportable pending a qualified assessment, rather than excused by default. The FDA clock is not applied to a device that is not placed on the US market. Each deadline is stated with the sub-paragraph that sets it, and the citations themselves are held to the regulation by test. A report is stood down only by a recorded determination — the regime, the ground from a fixed vocabulary, the reasoning, the decider and their qualification, with the platform's own suggestion of that moment kept beside it — never by editing the triage answers, which stay as the complainant gave them; a flagged complaint closes on a filed report or such a determination. A customer's survey answer, its feedback record and its audit entry are written in one transaction, so an answer describing harm cannot be thanked for and lost, and the respondent's address never enters the hash-chained trail. A customer message that reaches the helpdesk and alleges anything wrong with a device is put to a person as a possible complaint, on the customer's own words as well as on how it was triaged, and never on our own replies; recording it carries the customer's account and the moment the message arrived — which is when awareness began — into the complaint file rather than leaving either to be retyped; and deciding it is not a complaint takes a written reason and its author, as FDA QMSR §820.10 (ISO 13485:2016 §8.2.2) requires of a complaint that is not investigated. Complaints that were found not to be serious incidents are counted each night against the observation period, baseline and significance threshold recorded in that product's PMS plan, as EU MDR Article 88(1) requires the manufacturer to specify them, and a rise that meets the plan's threshold is raised as a signal with every number a reviewer needs to recompute it. Serious incidents are excluded from that count, since they are reported individually under Article 87. No signal is raised while the records do not yet cover the whole baseline, nor below the plan's floor of events, so a newly entered product and a rise from one event to two are both left alone.
complaint-gates.integration.test.ts (against a real database, as a signed-in user):
- will not close an EU-reportable complaint while the report is outstanding
- will not close an FDA-reportable complaint while the MDR is outstanding
- will not record a report against a complaint that is not flagged
- closes once the EU report is recorded with its reference, and the trail shows both
- closes an unflagged complaint without a report
- nobody is told while the triage says there is nothing to report
- the responsible people are told the moment it becomes reportable, and the actor is not
- and the audit trail says who was told
- records the date given, not today
- refuses a day in the future
- refuses a closure that says nothing about the investigation
- closes once the record is there, and keeps it
- a kind somebody muted does not reach their inbox
- but a complaint does — no setting silences a reporting clock
reportability.test.ts:
- runs from the awareness date, not from now
- gives 2 days for a serious public health threat, whatever else is true
- gives 10 days for death or unanticipated serious deterioration
- gives 15 days for harm or a malfunction
- is not reportable when nothing happened, and says so without a date
- gives 5 days for a public health threat
- gives 30 days for death or serious injury, and for a malfunction
- presumes harm without a serious-injury or malfunction flag is reportable, pending a qualified assessment
- marks the clear cases as decided, not presumed
- does not apply to a device that is not on the US market, and says why
- assesses normally when the market is unknown
- names the right Article 87 sub-paragraph for each deadline
- never cites 87(6) or 87(7), which are not the reporting deadlines
- reads the product's markets first, then the organisation's
- counts MDSAP, which includes the FDA as a participating regulator
- is unknown when nothing is recorded, so the assessment stays conservative
- counts down and goes negative once missed
regulatory-citations.test.ts:
- pairs each EU deadline with the sub-paragraph that sets it
- does not offer an EU deadline of 30 days anywhere
- cites no MDCG 2022-21 for a PMS-plan section — that guidance is about the PSUR
- does not invent a 30-day EU vigilance deadline
- describes Article 88 trend reporting as triggered by a signal, not by the calendar
- still names the deadlines it does give, correctly
reportability-determination.test.ts:
- accepts a decision with a ground that belongs to it and reasoning of substance
- refuses a ground borrowed from the other decision
- refuses a ground that belongs to the other regime
- refuses a shrug, and says what the ground wants answered
- refuses an unknown regime, decision or ground rather than storing it
- offers each regime a way to say yes and several ways to say no
- gives every ground a clause and a prompt, so the form can explain itself
- has a ground for triage answers entered in error, in both regimes
- closes when nothing was reportable
- refuses while a flagged report is unfiled, naming the way out
- closes once the report is filed
- closes on a determination that it is not reportable after all
- is not satisfied by a determination that it IS reportable
- checks both regimes, and the EU one first
reportability-determination.integration.test.ts (against a real database, as a signed-in user):
- keeps the decision, the ground, the reasoning, the decider and the suggestion it was made against
- refuses a shrug for reasoning, and refuses a ground from another regime
- supersedes the previous determination for the regime and keeps it readable
- will not un-file a report that has already been filed
- holds one live determination per regime, per complaint
- refuses closure while a flagged report is unfiled, and names the way out
- closes once the determination stands the report down
- refuses to clear a reportable flag by unticking a box, and points at the determination
- allows the edit once a determination has stood the report down
- still allows an edit that adds a flag
- is given no FDA deadline, so its complaint is not held open by one
survey-intake.integration.test.ts (against a real database, as a signed-in user):
- writes the answer, the feedback record and the audit entry together, linked
- keeps the respondent's address out of the audit trail
- refuses a second submission, an expired link and an unknown token, without half-writing
- refuses a missing required answer before anything is written
- has no silent catch on the writes
- does not tell a respondent a good link is unknown when the lookup failed
- writes the three rows in one transaction
complaint-vocabulary.test.ts:
- can say a post-market survey brought it
- keeps the values the existing records already carry
- separates the roles the regulation separates
- keeps a distributor distinguishable from a user in the label, not only the code
- gives every value a label, in both vocabularies
- offers every country, not a shortlist
- names them, and sorts by the name
- names them in the reader's language where the platform has one
- holds no OTHER, because that is not a country
- accepts a real code in either case and refuses a made-up one
ticket-complaint.test.ts:
- asks about the message that started all this
- asks when triage called it a product problem
- asks when triage raised the priority
- asks on the customer's own words alone
- does not ask about an ordinary question
- reads only what the customer wrote, not what we replied
- is when the customer's first message arrived
- takes the earliest customer message, whatever order they are given in
- falls back to when the ticket was opened if no customer wrote
- carries the customer's own words, not a summary of them
- says which ticket it came from
- keeps only what the customer wrote
- sets the awareness date to the day the message arrived
- carries the contact details already recorded
- maps how it reached us into the complaint module's words
- guesses nothing that decides a reporting deadline
- stays inside the field limits the intake form enforces
- refuses an empty reason
- refuses whitespace dressed up as a reason
- accepts a reason that says something
- asks for at least a sentence
trend-detection.test.ts:
- puts the baseline immediately behind the current period, newest first
- counts a boundary event once
- raises one when the period stands well above a steady baseline
- raises one when events appear where there were none
- stays quiet below the floor of events the plan sets
- stays quiet when the rise is within ordinary variation
- refuses to compare against a baseline that predates the records
- compares normally once the records cover the whole baseline
- reaches a different answer under a stricter threshold
- reaches a different answer under a longer observation period
- ships defaults that are a starting point, not a rule
- shows every number a reviewer would need to recompute it
- states a signal and refuses to state a conclusion
- does not invent a standard deviation it could not compute
- does not raise again while one is still open
- raises again once the open one has been dealt with
- raises again when the last one is older than the observation period
What they do not cover — and so what your PQ must. The reportability decision itself — whether an event is a serious incident, whether a malfunction would be likely to cause harm if it recurred — is made by your vigilance officer on the facts. The platform computes the deadline from the flags you set, presumes reportability where the flags are ambiguous, and records the decision your officer makes against it, with the ground and the reasoning; it does not make that decision. The content of the manufacturer incident report and its submission to the competent authority or the FDA happen outside the platform. For trends, the platform detects a rise against the numbers you set and raises it; whether that increase could significantly change the benefit-risk determination — the question Article 88(1) actually asks — is your vigilance officer's to decide and record, as is whether to open a CAPA. Trending is over frequency only: severity trending would need a graded severity on every complaint, which the platform does not yet capture.
M-MR — Management review
Your obligation: ISO 13485 §5.6 · ISO 13485 §5.6.2 · ISO 13485 §5.6.3
What our tests establish. A review is conducted only from the planned state and closed only from the conducted state; it cannot be closed until the §5.6.3 outputs — improvement, resources, product — are recorded; a closed review can no longer be edited; only quality administrators may run one; each transition is on the trail. The actions a review decides are recorded while it is conducted, each with what is to be done, an owner who is a current member of the organisation and not a read-only auditor, a due date not in the past, and the §5.6.3 output it answers — (a) improving the quality system, (b) improving the product, (c) a change for new or revised regulatory requirements, (d) resource needs. Each is a task linked to its review, so it appears in its owner's My Work, on the tasks page and on the dashboard. Closing one, as done or as not applicable, takes a written reason, which goes on the trail with its author and the status it replaced; reopening takes one too; the tasks page's reasonless controls refuse a review's action. Any change to an action takes a reason, with the values before and after on the trail. A closed review takes no new action and keeps what it decided and the output each action answers, while an open action's owner and due date may still change; the closed review's page and its print show every action with its state and closing reason. When the next review is held, the actions of earlier reviews still open are frozen among its §5.6.2 inputs as follow-up of previous reviews; closed ones are not.
management-review-gates.integration.test.ts (against a real database, as a signed-in user):
- is not something an RA user can open
- is planned, then conducted — and cannot be closed straight from planned
- keeps the inputs frozen at conduct, with the preparation beside them, and refuses to rebuild them
- cannot be closed until the §5.6.3 outputs are recorded
- is frozen once closed, and the trail shows each transition with its previous state
review-actions.test.ts:
- is accepted with what, a member as owner, a due date and the output it answers
- needs the owner to be a current member who can work on records
- needs a due date, and not one already past
- needs to say what, and which §5.6.3 output it answers
- is only possible while the review is conducted — not before it is held, and not once it is closed
- stores the four §5.6.3 outputs as the clauses ISO 13485:2016 letters them
- links the task to its review through the reference that says what raised it
- closes from open or in progress, and a closed one can only be reopened
- needs a reason to close it — its completion record — whether done or not applicable
- needs a reason to reopen one, and none to start work
- takes a reason, and refuses a change that changes nothing
- holds the same owner and due-date rules as recording one
- refuses a closed action until it is reopened
- keeps what a closed review decided, while its owner and due date may still change
- lists the open actions of every earlier review, not closed ones, not its own, not obligations
- counts open, closed and overdue the way the page, the dashboard and the assistant say them
- is read for display if anything is in it, and never breaks the page
- is written by nothing in src
- no longer promises a roadmap, and the dashboard it names counts the actions
- feeds the assistant the recorded actions, not the retired column
- letters §5.6.3 as the standard does
- prints the actions
- record an action as a task linked to its review, owned by a member, on the trail
- refuse an owner who is not a member, before writing anything
- close an action only with a reason, which goes on the trail with the previous status
- refuse, writing nothing, when the action changed between reading and writing
- are the only way to move one: the tasks page's buttons, which take no reason, refuse it
review-actions-gates.integration.test.ts (against a real database, as a signed-in user):
- are refused before the review is held
- need an owner who is a member, and a due date that is not past
- are recorded as tasks linked to the review, in the owner's My Work and on the tasks page
- close only with a reason, which is kept on the trail; the tasks page's buttons cannot close one
- are not added to a closed review, which keeps what it decided while follow-up goes on
- the next review is shown the earlier review's open action among its inputs, and not the closed one
What they do not cover — and so what your PQ must. Whether the §5.6.2 inputs presented were complete and current, and whether the decisions taken were the right ones, is the review's own business. The platform assembles inputs from its records and holds the outputs; it does not assess them. Whether an action was really done, and whether the reason given for closing it is adequate, is the reviewer's judgement: the platform requires that a reason is written and keeps who wrote it, not that it is true. No reminder email is sent for an action falling due; it is shown as overdue on the review, the dashboard and the owner's My Work.
M-PMS — Post-market surveillance — proactive feedback
Your obligation: EU MDR Article 84 · EU MDR Annex III §1 · EU MDR Article 86 · EU MDR Article 87(1) · ISO 13485 §8.2.1
What our tests establish. A shipment becomes eligible for a feedback survey only after the delay its PMS plan sets, and only if it was recorded after the programme was switched on — enabling a programme does not survey the organisation's history. A shipment under recall or under review is never surveyed, and that check comes before every other; an address that has objected, hard-bounced or been erased is never written to; the same contact is not asked again inside the plan's interval; and a shipment with no usable feedback address is recorded as skipped with its reason rather than passed over in silence. The address is matched by a per-tenant salted hash, so the rule survives anonymisation and does not correlate a person across organisations. One reminder is sent, never two, and never after the link has expired. A send that was claimed and not confirmed is marked failed for a person, never re-sent by a machine. In the survey instrument the safety questions are required, carry no branching condition, and precede every satisfaction question; they ask about plain events rather than categories, and carry no Net Promoter question. Any answer other than "No" flags the response by rule at receipt, "I am not sure" exactly like "Yes", and confusing labelling that led to a mistake screens as a product problem because the instructions are part of the device. Every reason that applies is recorded, not only the most serious. The triage suggested to the complaint form is a malfunction and a harm and nothing else — never seriousness, never a public-health threat. The email itself names the manufacturer as controller and states the surveillance purpose, offers a way out in the RFC 8058 headers as well as the body, shows the survey link as an address and not only as a button, always gives a way to report a problem that does not depend on the link, and sells nothing — one promotional sentence would turn a surveillance message into marketing, which needs a consent nobody obtained. Its display name cannot be made to forge a second address. The opt-out token is a keyed hash of the tenant and the address hash: it cannot be forged, it identifies nobody who intercepts it, and it keeps working after the address itself has been erased, because the suppression list has to outlive the data it protects. A flagged response that nobody has determined escalates to the back-up reviewer, the quality administrators and the designated person responsible for regulatory compliance at 24 hours and to regulatory affairs at 48, driven by the scheduled job rather than by somebody opening the queue, and never repeating a step it has already taken; something already two days old goes straight to the second step rather than climbing one a day. Escalation stops the moment a determination is recorded. A message the provider reports as permanently rejected, and a recipient who marks one as spam, both suppress the address by hash; a temporary failure — a full mailbox, a server down for an hour — never does, because suppressing on it would silently stop writing to a customer who never asked for that and cost the manufacturer a channel it is obliged to run. The periodic safety report carries what the programme did: what was surveyed, what came back, what was flagged, what became a complaint, and how quickly somebody looked — every rate stated with its denominator, and the shipments that were not surveyed reported with their reasons rather than hidden, since a response rate over the shipments that could be asked is not a response rate over the distribution. The plan's own paragraph describing the programme is generated from the settings that run it — the delay, the reminder, the expiry, the cap, the approval mode and the escalation chain — in roles rather than names, so a document and a scheduled job cannot drift apart and a plan does not name somebody who left. That paragraph always states that consignee answers are distributor and importer feedback and not user feedback, that awareness is receipt rather than reading, that shipments under recall are never surveyed and are contacted through the field safety process instead, and that every response ends in a recorded determination. The survey and its email exist in all 23 languages the application routes; no language is missing a string, no placeholder was lost in translation, the stored answer values are identical across languages so an answer given in Greek counts as the same answer as one given in English, the safety questions remain required and unconditional in every language, and none of them has drifted into the clinical register. A manufacturer may replace the wording of any question in any language and add up to three questions of its own; what it cannot do is remove a safety question, make one optional or conditional, move one after the satisfaction questions, change what an answer is stored as, or rewrite a safety question as a statement that gives the reader nothing to answer. Wording of its own is warned about, never refused, when it asks the reader to classify rather than to describe. Each language is edited separately, English is the fallback for any language left unwritten, and every change is recorded with the text before and after — on an active plan with a reason as well. Contact details of somebody surveyed are cleared once their cycle has closed, nobody still owes it a determination and the tenant's retention period has run, with a floor of one year that a lower setting cannot go under; what they said, the determination recorded about it and the audit trail are never scrubbed, because they are the quality record. Clearing the contact data changes nothing the periodic report says, which is asserted by summarising the same cycles either side of the scrub rather than assumed. An erasure request clears every copy of the name and address at once regardless of the clock, cancels anything queued to that address, records the objection as a hash so it goes on being honoured, and produces a reply stating what was removed, what was kept and under which exemption. The periodic report an active plan owes under EU MDR Article 86 is reminded about two months out, a fortnight out, on the day and weekly once late, counted from the date on the plan or — where none was set — a cadence after the plan was signed; a month-end due date does not roll into the following month; and approving the report moves the next due date on by a cadence, so a plan is never reminded about a report that has been filed. A customer who answers by replying to the email rather than clicking the link is matched to their own survey by the Message-Id we generated for it, not by a subject line or an address, and their reply becomes a response on that cycle with a review row like any other. Keywords in the recipient's language and in English can raise a flag on such a reply; nothing clears one — a reply with no keyword hit is put to a person, because the phrases 'nothing to report' and 'nothing worked' share a word and no list can establish the absence of a problem. A reviewer may flag a response the rules let through and may lift a flag the rules got wrong; both take a written reason and their author, both are recorded beside what the rules found rather than over it, and neither is possible once the response has been determined. Lifting a flag does not determine anything — it takes the response out of the queue, and it still needs a person's determination, because the closure rule never closes a response the rules flagged. A response that no rule and no reviewer flagged is closed after five working days by a versioned rule that records itself, so the promise of a determination on every response holds for the quiet ones too; a flagged response is never closed that way however long it waits, because that one escalates and is a person's to answer — the phrase "adverse event" and its equivalents are held out by test, because a question in that register collects fewer reports than one about plain events. The people who actually used a device are reached in one of two ways, and the difference is what the platform ends up holding. A forwardable link is issued once per shipment, travels inside the consignee's own survey email, and is answered by whoever it reaches: no name, no address and no identifier of a respondent is collected, and the database refuses to store one — a cycle that arrived through a link cannot carry a recipient name, a recipient address or a send time, and cannot be queued, sent or reminded, whatever code attempts it. The link expires, stops at a cap that is enforced as a constraint rather than a convention so two simultaneous answers cannot both pass it, is never issued for a shipment under recall, is refused at submission if a recall began after it went out, and can be withdrawn by the manufacturer with what it already collected left intact. Because no one is addressed, no response rate can be computed from it: those answers are counted on their own line of the periodic report, with the reason stated, and they never move the rate for the shipments that were addressed. Everything downstream is identical to an emailed answer — the same screening rules, the same review row, the same queue, the same awareness date at receipt — because the route an answer took must never change what happens when somebody reports harm. The alternative, holding contact details supplied on the shipment, cannot be switched on without a recorded attestation: five statements confirmed, a lawful basis written in the manufacturer's own words rather than chosen from a list, the person's name and role and the date, and the wording stored as displayed so that improving the screen later cannot change what was agreed. The database refuses that mode without it, and refuses a basis too short to be one. Patients are excluded by that attestation rather than by a check no schema could make, and the first message to a supplied contact carries what Article 14 requires, including which organisation provided the details. A programme that would ask nobody at all is refused.
pms-feedback-selection.test.ts:
- is the same for the same address however it was typed
- differs between tenants for the same address
- rejects what is not an address
- is the delay after it shipped, not before
- ignores shipments from before the programme was switched on
- never surveys a shipment under recall
- never surveys a shipment under review either
- checks the recall before anything else
- does not write to an address that has objected, bounced or been erased
- does not ask the same person again inside the cap
- asks again once the cap has passed
- honours a cap of zero as no cap at all
- records a shipment with no usable address rather than passing over it
- stops when the programme is paused
- sends when nothing stands in the way
- reminds once, after the interval the plan sets
- never reminds twice
- does not remind somebody to click a link that has already died
- does not remind at all when the plan asks for no reminder
- expires a link that was never answered
- is picked up for a person to look at, not re-sent
- leaves a send that is still in flight alone
pms-feedback-screen.test.ts:
- never hides a safety question behind another answer
- requires an answer to each of them
- asks about safety before it asks about satisfaction
- carries no Net Promoter question
- asks about plain events, not categories
- opens the detail and consent questions only when something is reported
- lets a clean set of answers through
- flags reported harm above everything else
- flags a reported product problem
- flags both together when both are reported
- treats confusing labelling that led to a mistake as a product problem
- routes 'I am not sure' to a person, exactly like a yes
- keeps every reason, not only the most serious one
- flags somebody who asked to be contacted, even with nothing else
- flags a shipment that never arrived
- survives a response with nothing in it
- names every flag in words a reviewer can act on
- suggests the two triage answers the survey can actually support
- suggests nothing about seriousness or public health
- suggests nothing at all from an unsure answer
pms-feedback-unsubscribe.test.ts:
- verifies the one it produced
- refuses a token for another address
- refuses a token from another organisation
- refuses a guess, whatever its length
- does not contain the address it protects
- names the controller and the purpose
- offers a way out in the headers as well as the body
- shows the link as an address, not only as a button
- gives a way to report a problem that does not depend on the survey
- says when the link stops working
- sells nothing
- identifies the shipment the recipient actually received
- replies go somewhere a person reads
- keeps the way out and the way to report
pms-feedback-escalation.test.ts:
- leaves it alone inside the target
- goes to the back-up at 24 hours
- takes its second step, to regulatory affairs, at 48
- never repeats a step it has already taken
- goes straight to the second step when it is already that late
- stops once somebody has determined it
- ignores a response that was never flagged
- keeps the two thresholds a day apart
- names the flag, the hours and the clock that is running
- says awareness began when the answer arrived
pms-feedback-delivery.test.ts:
- suppresses when the address rejected it for good
- does not suppress a temporary failure
- treats an unrecognised bounce type as temporary
- treats a bounce with no type at all as temporary
- suppresses at once
- records a delivery
- leaves a delayed message alone
- ignores an event about nothing
pms-feedback-psur.test.ts:
- counts anything that went out as sent, answered or not
- states the rate over what was actually asked
- reports the skips with their reasons rather than hiding them
- says plainly when the rate does not describe the whole distribution
- gives no rate at all when nothing was sent
- separates what became a complaint from what did not
- does not count a clean response as awaiting a determination
- measures how quickly the organisation looked
- says so plainly when nothing was reported
- never move the response rate
- are counted on their own line, with the reason there is no rate
- are not counted among the shipments considered for a survey
- say nothing at all when none arrived
- treat a row with no origin as an ordinary addressed send
- are reviewed exactly like an emailed one
pms-feedback-plan-text.test.ts:
- states the delay, the reminder, the expiry and the cap as configured
- says so when no reminder is configured
- says so when there is no frequency cap
- describes the approval mode that is actually in force
- refuses to call consignee feedback user feedback
- states that awareness is receipt, not reading
- states the recall exclusion and where those customers are contacted instead
- states that the skips are reported with the response rate
- states the escalation chain and the review target
- promises a determination on every response, not only the flagged ones
- uses roles rather than names
- falls back to a role that always exists when nobody is named
- describes the link, and that nothing about a user is collected
- says answers through a link are counted and never rated
- keeps the recall exclusion, which applies to a link as much as an email
- states the Article 14 duty and the basis when addresses are held
- says patients are not contacted
- does not claim a consignee was surveyed when none was
- still names the consignee's answers as economic-operator feedback when it does ask them
- connects the two sections so they read as one process
pms-feedback-gates.integration.test.ts (against a real database, as a signed-in user):
- refuses a second cycle for the same shipment, audience and person
- allows a cycle for a different address on the same shipment
- refuses a skip with no reason
- refuses a survey that claims to have been sent with no send time or link
- refuses a determination of not-a-complaint with no rationale
- refuses clearing a harm report with no second look
- refuses the same person confirming their own decision
- refuses a proposal with no reason worth the name
- accepts a second, different person — and the one-person exception, on the record
- refuses a determination of complaint with no complaint to point at
- refuses a rationale too short to be a reason
- records the determination with its reason, its author and the moment
- will not determine the same response twice
- takes its awareness date from when the answer arrived, not from today
- carries the shipment across so nobody retypes it
- quotes the respondent's own words rather than a summary of them
- takes the triage the answers actually support
- suggests nothing about seriousness or public health
- links the feedback to the complaint in both directions
- puts the raising on the audit trail with where it came from
- is stored without a send time, which an addressed one could not be
- cannot be given an address, however it is written
- cannot be queued, sent, or reminded
- still cannot be answered without the distribution holding the answers
- stops accepting answers at its cap
- is issued once per shipment
- refuses a programme that holds end-user addresses with no attestation
- refuses a programme that asks nobody at all
- the trap: 'not ADDED' on its own leaves out every response with no hand flag at all
- the same trap in the old queue filter: NOT lifted also left out every untouched response
- the review queue holds the lifted one and the hand-flagged one, not the quiet one
- quiet closure considers the quiet one — and only it
- tells the responsible people, and records the step so it cannot repeat
- sends the same words by email — an inbox row only reaches somebody signed in
- running again changes nothing — the step is already taken
- saves the answer, flags it, and tells the reviewers
- says in the notice that nobody can be written back to
pms-feedback-i18n.test.ts:
- carries every language the application routes
- leaves no string missing in any language
- keeps every placeholder the English has
- does not leave an untranslated language silently blank
- asks about plain events, not clinical categories
- offers an unsure answer distinct from yes and no in every language
- keeps the line that comes before the survey's own interests
- translates the questions while keeping their ids
- keeps the stored answer values identical across languages
- still requires the safety questions in every language
- prefers what the shipment recorded
- falls to the consignee's country
- then to the organisation's own language, then to English
- reports whether the wording has been reviewed by a speaker
- replaces what it knows and leaves what it does not
- handles a translation that moved the placeholders around
pms-feedback-customisation.test.ts:
- replaces a question with the tenant's own words
- keeps each language's wording separate
- falls back to the platform's wording where the tenant wrote none
- treats an emptied field as reverting to the platform's wording
- refuses wording for a question that does not exist
- refuses to let a safety question stop being a question
- allows a reworded safety question that is still a question
- refuses wording longer than the field can carry
- cannot change what an answer is stored as
- cannot make a safety question optional or conditional
- appends them after the fixed set
- never makes one required
- shows them in the reader's language, falling back to English
- caps how many there can be
- needs English as the fallback every language can reach
- needs at least two answers when it is a choice
- keeps their ids out of the fixed set's namespace
- warns when a safety question asks the reader to classify
- warns in the language the wording was written in
- says nothing about wording that describes an event
- ignores anything that is not wording
- ignores a stored question that could shadow a fixed one
- never reads back more than the cap
pms-feedback-retention.test.ts:
- clears it once the period has run
- leaves it while the period is still running
- leaves a cycle that has not closed
- leaves a response still awaiting a determination, however old
- does not clear twice
- has nothing to clear when no address was ever held
- will not go below a year, whatever is configured
- honours a longer period
- defaults to a year when nothing is configured
- applies the floor to the decision, not only to the number
- produces the same figures before and after the scrub
- reads no field that the scrub clears
- says what was removed
- says what was cancelled
- says what was kept and under which exemption
- says the complaint keeps its contact details, and why
- explains the fingerprint that has to stay
- leaves out the complaint paragraph when there is no complaint
pms-report-due.test.ts:
- uses the date on the plan
- counts from the signature when no date was set
- has no clock for a plan nobody signed
- does not roll a month-end into the next month
- advances a cadence from the period that was approved
- says nothing two months and a day out
- speaks up two months out, when there is still time to gather the data
- speaks up again a fortnight out
- speaks on the day
- keeps speaking once it is late, with the count
- does not repeat a milestone it has already given
- repeats weekly once it is late, and not more often
- says nothing about a plan that is not active
- says nothing when there is no clock to run
- names the device and the date
- calls a two-yearly report biennial
- says what a missing report means at audit
- says why two months is the moment to start
pms-feedback-reply.test.ts:
- picks up the id we generated, from either header
- puts the message being answered first
- ignores somebody else's thread
- flags a reply that reports somebody hurt
- flags a reply that reports something broken
- flags both when both are described
- reads the recipient's language and English together
- does not clear a reply just because no keyword matched
- reads every language the survey goes out in
- still puts a pleasant reply in front of a person, in any language
- does not clear an empty reply
- keeps what the customer actually wrote
pms-feedback-closure.test.ts:
- puts a rule-flagged response in front of a person
- leaves a clean one out of it
- lets a reviewer put one in that the rules let through
- keeps one whose flag a reviewer lifted — the decision is still owed (F-067)
- skips the weekend
- counts nothing across a weekend alone
- counts nothing backwards or for the same day
- closes it after five working days
- leaves it alone before then
- never closes a flagged response, however long it waits
- never closes one a reviewer flagged by hand
- never closes one whose flag a reviewer lifted
- does not determine the same response twice
- stamps the rule's version on what it closed
- asks for a reason worth reading
- accepts one that says something
pms-feedback-end-users.test.ts:
- is usable while it is live, unspent and unrevoked
- stops at the cap
- stops when it expires
- says revoked rather than expired when both are true
- lives longer than the personal link it travels with
- mints a token nobody is going to guess
- is keyed by an opaque value, one per answer
- does not collide with a real recipient's hash
- carries no name and no address
- is refused if anybody tries to put an address on it
- was never sent, so it cannot claim a send time
- is born answered
- leaves an ordinary emailed cycle alone
- reads the mode
- treats anything it does not recognise as reaching nobody
- accepts a complete one
- refuses until every statement is confirmed
- refuses a basis too short to be one
- reports every problem at once rather than one at a time
- is recorded against a person
- contains every statement, so the stored record is the whole undertaking
- leaves our commentary out
- says patients are excluded, in the statement rather than a footnote
- is versioned, so a reworded screen does not rewrite what was agreed
- names who supplied the details
- names the manufacturer, states the purpose, and offers the way out
sending-domain.test.ts:
- is read as the domain they meant
- accepts a subdomain
- refuses the root domain, and says what to use instead
- counts a multi-part public suffix as the root it is
- refuses our own domain, since that is what it already uses
- refuses what is not a domain, without saying merely 'invalid'
- stays on ours until theirs is verified
- moves to theirs once it is
- drops the 'via' only when the domain itself says who sent it
- cannot be made to forge a second address
- falls back to a name when the manufacturer has not set one
- stays on ours when the status says verified but no domain was stored
- is read into our own vocabulary
- does not read a temporary failure as a failure
- treats a part-way domain as not ready, in either direction
- keeps a verified domain verified through a check that did not complete
- keeps what a DNS entry needs and drops the rest
- drops anything incomplete rather than rendering a half record
- survives a provider that returns nothing at all
- is read out of either form somebody configured
- never nests one header inside another
- uses what the deployment configured rather than a hardcoded domain
- is ignored once the tenant has a domain of their own
What they do not cover — and so what your PQ must. Whether a response is a complaint is decided by your reviewer and recorded with a rationale; the platform flags and refuses to close a determination without one, but it does not decide. The delay, reminder, validity and frequency numbers are the plan's, as EU MDR Article 84 and Annex III intend, and choosing them is your judgement about your device. Deliverability is not something the platform can promise. Mail goes out from the platform's own domain by default, whose reputation is the operator's responsibility; a manufacturer may instead send from a subdomain of their own, and from that point its reputation, its DNS records and anything that damages them are theirs. The platform refuses a root domain, keeps sending from its own until the records verify, and will not move mail onto a part-verified domain — but whether a given mailbox accepts a message is the recipient's provider's decision, and no configuration here makes it otherwise. Consignee answers are distributor, importer or procurement feedback and must never be presented as user feedback; that distinction is stated in the generated plan paragraph and is not one the platform can enforce for you. Where end users are reached by a forwarded link, the platform cannot know who answered, cannot tell whether one person answered twice, and cannot say how many people the link reached — so it reports those answers as a count and refuses to state a rate, and any statement in your own documents about user coverage is yours to justify. Whether the people you reach that way are the people who used the device depends on the consignee forwarding it, which is outside the platform entirely. Where you instead record end-user contact details, whether you may hold them is your determination: the platform captures the attestation, the lawful basis you state and who made it, and refuses the mode without them, but it does not assess the basis, cannot detect a patient behind an address, and does not carry out your balancing test or impact assessment. Keeping that list accurate, and passing objections into the platform so they are honoured, is the undertaking you sign. The survey and its email exist in all 23 languages the application routes, but Danish was reviewed by the operator on 2026-09-09 — the safety questions and their answers, and again for the wording the forwarded link added — and is marked as such; the remaining 21 non-English translations were produced by the platform's author and have NOT been reviewed by a speaker — the operator decided to ship them on that basis on 2026-09-08. What can be checked mechanically is checked by test: no missing string, no lost placeholder, identical stored answer values, the safety questions still required and unconditional, and no drift into the clinical register. What cannot be checked that way is whether the safety questions read plainly in each language rather than sounding clinical or accusatory, and that is a judgement only a speaker can make. Every send records which language it went out in and whether that wording had been reviewed, so the gap sits on the record rather than being assumed away; a language moves into the reviewed list in one line once somebody has checked it. The same caveat covers the word lists that read an emailed reply: they are the author's, unchecked by speakers, and their failure mode is one-sided by design — a missing word means the reply is put to a person rather than cleared.
M-SVC — Servicing — analysed for complaints
Your obligation: ISO 13485 §7.5.4 · ISO 13485 §8.2.2 · FDA QMSR §820.10 · EU MDR Article 87(1)
What our tests establish. Every servicing record is analysed for a complaint when it is created, and every record ends in one of three recorded states — raised as a complaint, determined not to be one, or owing that determination and listed as owing it. The distinction the analysis turns on is not whether a repair happened but whether somebody told the manufacturer something was wrong: scheduled maintenance with nothing reported is not a complaint, and the same visit made because a customer reported a fault is, because the customer's report is the communication ISO 13485 §8.2.2 describes and the visit is only how its meaning was established. Corrective servicing, a customer report recorded against the visit, and words in the engineer's own narrative describing the device not performing are three independent signals, any one of which puts the record to a person; every reason that applied is recorded rather than the strongest one. Wording that describes somebody being hurt or nearly hurt is flagged as harm and can never be closed by rule, whatever category the service was logged under, because the reporting clock is already running by the time an engineer writes it down — the database refuses that combination rather than trusting the application to remember it. The screening rule may close only the quiet case, and records its own version on the row it closed, so a decision taken by machine can be explained years later by the rule that took it. A person deciding a service was not a complaint gives a written reason of at least twenty characters and is named; a record said to be a complaint names the complaint it became; a determination without a moment or without an author cannot be stored at all. The awareness date is held in its own column and is the day the customer reported the problem, not the day the engineer arrived and not the day the paperwork was filed — EU MDR Article 87(1) and 21 CFR §803.50 run from awareness, and where no report date was recorded the complaint form says the date was inferred rather than presenting the service date as though somebody had checked it. The complaint raised from a servicing record carries the engineer's own words, the device's serial or lot, and that awareness date, and leaves the malfunction, harm and complainant-type answers blank, because those three set the reporting deadlines.
servicing-complaint.test.ts:
- is not a complaint, and the rules may say so themselves
- still records why it reached that conclusion
- covers the other planned activities too
- is presumed a complaint when the service was corrective
- is presumed a complaint when the customer reported it, whatever the category says
- reads the engineer's plain words, not a clinical vocabulary
- gives every reason that applies, not the strongest one
- is flagged as harm and never closed by rule
- outranks the servicing category it was logged under
- says harm rather than merely deficiency when both appear
- catches a near miss, which matters as much as an injury
- is when the customer reported it, not when the engineer arrived
- falls back to the service date, and says that it did
- carries the engineer's own words and the device it was about
- carries the awareness date the customer's report sets
- leaves malfunction, harm and complainant type to the person filling it in
- marks an inferred awareness date so the reviewer can correct it
- takes a sentence, not a word
- names the clause an auditor will ask about
servicing-gates.integration.test.ts (against a real database, as a signed-in user):
- starts as owing one — never as an answer nobody gave
- is a moment when it is made, and nothing when it is not
- has an author — a person, or the rule that made it
- takes a written reason from a person who says it is not a complaint
- lets the screening rule close the quiet case, under its own version
- names the complaint when it says there is one
- is never closed by a rule
- can still be determined by a person, with their reason
- comes from a vocabulary rather than free text
- keeps 'nobody asked' distinct from 'they said no'
- is stored apart from the service date, and can precede it
What they do not cover — and so what your PQ must. Whether a serviced device's record is a complaint is your determination; the platform screens, refuses to leave the question unanswered, and refuses a determination without a reason and an author, but it does not decide. The word lists that read an engineer's narrative are English, and they are a net rather than a classifier: a record whose wording they do not match still reaches a person as PENDING, so their failure mode is a needless prompt rather than a missed complaint, but a service report written in another language will not be screened by them. Whether the report date entered against a visit is the date the customer actually told you is a matter of your own records — the platform stores what is entered and says when nothing was, and cannot detect a date that is simply wrong. Servicing records created before this analysis existed are listed as owing a determination rather than being decided retrospectively; working through them is yours. The platform does not schedule servicing, does not track engineer competence or the calibration state of their tools, and does not verify that the work described was actually performed.
M-SUP — Supplier control
Your obligation: ISO 13485 §7.4.1 · ISO 13485 §7.4.2
What our tests establish. A supplier code is unique in your organisation; qualification moves through pending, under review, approved, conditional and disqualified; approval stamps the date; a status the register does not have is refused; every change is on the trail with the previous status; an auditor can read the register but not change it. A qualification questionnaire is attached to a supplier from a template and answered, each answer on the trail, and an auditor cannot answer it. Each answer keeps a copy of the question it answered — the wording, guidance, weight and whether it is critical — so a built-in question that is reworded or removed afterwards neither changes nor deletes the answers already recorded against it.
supplier-gates.integration.test.ts (against a real database, as a signed-in user):
- refuses a second supplier with the same code
- starts unqualified
- is approved with a dated stamp, and the trail records the previous status
- can be disqualified, and the register says so
- rejects a status the register does not have
- is not something an auditor can change
supplier-questionnaire-answers.integration.test.ts (against a real database, as a signed-in user):
- is attached from a shared template, one pending answer per question, each with its own copy
- attaching it again adds nothing
- is answered, and the answer is on the trail
- is not something an auditor can answer
- still reads with the wording that was answered after the shared question is reworded
- is kept, and still reads as answered with the wording asked, after the shared question is removed
- can still be amended after the question is removed, keeping the wording asked
- is kept, under the template's name, after the whole shared template is removed
What they do not cover — and so what your PQ must. Your evaluation criteria, the supplier audits and questionnaires behind a status, quality agreements and their content, and the re-evaluation interval are yours. The platform records the outcome of your evaluation; it does not perform it.
M-TRN — Training and competence
Your obligation: ISO 13485 §6.2
What our tests establish. Training is assigned to a named person for a named topic; the trainee completes it, and a colleague of ordinary rights cannot complete it for them; it cannot be verified before completion; the verifier must be a different person from the trainee, by identity rather than by role; the record says who verified and when.
training-gates.integration.test.ts (against a real database, as a signed-in user):
- assigns training to a named person for a named topic
- cannot be verified before it is completed
- is completed by the trainee, not by a colleague of ordinary rights
- is verified by someone other than the trainee, and the record says who and when
- assigns everyone in the target roles when it first becomes effective
- does not assign the same revision twice
- assigns revision 2 to the very people who were trained on revision 1
- tells them, and says that it is a revision of something they know
What they do not cover — and so what your PQ must. Whether the person is competent — the assessment behind the verification — and the content and effectiveness of the training are yours. The platform records that training was assigned, completed and verified by separate people; it does not examine anyone.
M-EQP — Equipment and calibration
Your obligation: ISO 13485 §7.6 · ISO 13485 §6.3
What our tests establish. An asset code is unique; a status change is recorded with its reason and previous state; only quality or manufacturing roles may record a calibration certificate; a certificate's due date must fall after the date performed; recording one moves the equipment's next-due date; only quality administrators may delete a certificate.
equipment-gates.integration.test.ts (against a real database, as a signed-in user):
- refuses a second asset with the same code
- records a status change with its reason and the previous state
- is not something an auditor can change
calibration-gates.integration.test.ts (against a real database, as a signed-in user):
- refuses a certificate from a role that is not QA Admin or Manufacturing
- refuses a calibration recorded as a date alone (B-16)
- refuses a due date that is not after the performed date
- records a certificate from Manufacturing and moves the equipment's next-due date
- the database refuses what the action refuses — a performer kind with no performer, an unknown result
- refuses to let anyone but QA Admin delete a certificate
What they do not cover — and so what your PQ must. The validity of a certificate, its traceability to national standards, and the impact assessment when an instrument is found out of tolerance are yours. The platform keeps the dates and the documents; it does not calibrate anything.
M-SW — Software lifecycle (IEC 62304)
Your obligation: IEC 62304 §9
What our tests establish. A problem report is numbered against its software item; it cannot be closed until a root cause and a resolution are recorded and verified; an engineer can report and resolve but not close; the trail shows the path.
software-gates.integration.test.ts (against a real database, as a signed-in user):
- an engineer reports a problem, and it is numbered against its item
- cannot be closed before a resolution is recorded and verified
- is resolved with a root cause and a resolution, which leaves it waiting for verification
- cannot be closed while its resolution waits for verification
- is not verified without a note of how, by its resolver, or without approval rights
- is verified by somebody else with approval rights, and the trail says how
- is closed only by someone with approval rights, and the trail shows the path
- an edit does not release or retire the item, and a row released before that stays as it was
- approves a SOUP item, and the record names who approved it and when
What they do not cover — and so what your PQ must. Only problem resolution (§9) is covered. Safety classification, the development and maintenance processes (§5–§8), SOUP evaluation and verification are the software team's, and the platform's records of them are not tested here.
M-USE — Usability engineering (IEC 62366-1)
Your obligation: IEC 62366-1 §5.9
What our tests establish. A usability file cannot be approved until the summative evaluation is completed and the residual risk is marked acceptable with a recorded rationale.
lifecycle-gates.test.ts › IEC 62366-1 UsabilityFile approval gate:
- approves when all conditions met
- blocks approval when summative not completed
- blocks approval when residual risk not marked acceptable
- blocks approval when residual rationale missing
What they do not cover — and so what your PQ must. The evaluation itself — its protocol, participants and findings — is yours. The gate checks that it was recorded as done and concluded; it cannot check that it was done well.
M-VAL — Process and software validation runs
Your obligation: ISO 13485 §7.5.6 · ISO 13485 §4.1.6
What our tests establish. A validation run must have a recorded result before approval; the approver must differ from the executor; a software validation cannot pass with deliverables outstanding but may fail at any point; a run that names an older platform version is reported as drifted, one that names no version as unknown rather than stale; and drift raises a task for your administrators once per version.
software-validation.test.ts:
- are T1 through T5, in the order they have to be done
- start out none done
- survives null, which is what a run from before this feature holds
- survives values that are not step state at all
- keeps what was stored
- drops a deliverable the pack no longer has
- defaults a deliverable the pack has gained, rather than stranding the run
- treats a truthy-but-not-true done as not done
- is false while any is outstanding
- is true only when all five are done
- is false for a run with no step state, not vacuously true
- says nothing while the run is still open
- flags a concluded run pinned to an older version
- does not flag a concluded run pinned to the current version
- does not claim drift for a run that never named a version
- flags a passed-but-not-yet-closed run too
- counts planned and in-progress, and nothing else
lifecycle-gates.test.ts › Validation run closure gate:
- closes a PASSED run that's been approved
- blocks a FAILED run without a linked CAPA
- allows a FAILED run with a linked CAPA
- blocks any run that hasn't been approved
revalidation-drift.test.ts:
- raises for a tenant whose latest concluded run names an older version
- is silent when the validated version is the current one
- does not claim drift for a run that never named a version
- ignores equipment and process validations
- lets the latest concluded run speak for the tenant
- stays quiet for a tenant already revalidating against the current version
- does not treat a FAILED run as a validation to measure drift from
- keys the task to the current version, so a daily run raises it once
- handles several tenants independently
What they do not cover — and so what your PQ must. Your protocols' content and acceptance criteria are yours — the platform is the record of the run, not its author. The IQ, this OQ and the PQ are our validation of the platform; a process you validate in it (a steriliser, a line) is validated by your protocol.
M-SUB — Regulatory submissions
Your obligation: EU MDR Annex II · EU MDR Annex III · 21 CFR 807 (510(k))
What our tests establish. Evidence is pinned to a named section of a submission and records what it points at; external evidence with neither a title nor a URL is refused; a pin can be removed and the removal is on the trail; a role without document rights cannot pin.
submission-gates.integration.test.ts (against a real database, as a signed-in user):
- will not declare a 510(k) ready for a Class III product without a reason
- refuses a reason too short to be one
- records the reason with who gave it, and then allows ready
- leaves an EU submission alone — there is no question to answer
- refuses external evidence with neither a title nor a URL
- pins external evidence to a section and records what it points at
- is not something an auditor can pin
- refuses an id that points at nothing
- accepts a draft document, and the score does not count it
- removes a pin, and the removal is on the trail
What they do not cover — and so what your PQ must. Whether the dossier is complete and whether the regulator accepts it are questions the platform cannot answer. The completeness gauge counts sections with evidence; it does not read the evidence.
M-TRACE — Traceability of units leaving the building
Your obligation: EU MDR Article 27(4) · EU MDR Article 25 · EU MDR Annex I §23.2(g) · EU MDR Annex I §23.2(h) · ISO 13485 §7.4.3 · ISO 13485 §7.5.9 · 21 CFR §801.109
What our tests establish. A label for a device that is implantable, supplied sterile or single-use cannot be issued without a lot or a serial number, so the printed carrier holds a production identifier and not only a device identifier; a template proof is the one exception, and every label a proof prints carries the words 'PROOF — NOT FOR SHIPMENT'. Whether the device is supplied sterile, how, and whether it is single use are recorded on the product, edited on its page with the change on the audit trail; the label and dispatch gates read them from there, a label template shows them read-only and cannot change them, and the printed label's sterile and single-use symbols follow them. A product that does not record one of them is treated as possibly sterile and single use, so its labels need a lot or serial and its shipments a production batch, and the refusal says the fact is not recorded rather than claiming it; the classification wizard's sterile answer may not contradict what the product records. Dates are refused where the expiry is not after the manufacture date, where the manufacture date is in the future, or where the expiry has already passed, at label issuance and at goods receipt alike. A Class IIa or above device sold in the EU cannot be labelled without its notified body's number beside the CE marking, and the printed label carries the UDI-DI under the UDI symbol and in both barcodes, never the Basic UDI-DI. Labels are refused unless the GS1 DataMatrix carrier can be encoded from the template's UDI-DI and the run's production identifiers. These label checks are one function, applied to every route that issues labels — the label template's own page and the production batch's print button alike. An issued run records everything its labels carry at the moment it is issued — the device, the market and CE marking, the notified body's number, the class it stood on, the UDI-DI, both barcodes' contents, the symbols and the manufacturer's details — and printing it prints that record, never the template or product as later edited; it prints only if the same checks would still issue it today, and a run with no such record (issued before 4 October 2026, or created outside the issuing routes) is refused with the reason and the way to issue a fresh run. A shipment must name the product shipped or the production batch it came from, and a linked production batch or a lot or serial number; a lot under a hold, or in any status but released, is refused before the record is created, whether or not a product was chosen, and a lot that cannot be checked against stock refuses the shipment rather than letting it through. Where the shipment's lot cannot be deducted from inventory, the reason is recorded on the audit trail and shown to the person who shipped, rather than passing silently. Material received from a supplier who is not approved is quarantined whatever the location's default, and new locations default to quarantine. The product identifier is normalised to fourteen digits in one place, so production and labelling cannot stamp different identifiers on the same device. Every shipment is recorded against a customer: a consignee with the same name, address and country as a customer already held — case and spacing ignored — is that customer, any other is a new customer with its own number, and two different addresses are never joined automatically. The shipment keeps its consignee exactly as shipped; correcting a customer never rewrites one. A recall drawn from shipments carries their customers and lists its consignees grouped by customer, so one hospital shipped twice reads as one recipient with two shipments. Two customers are joined only by a person with a reason of at least twenty characters; the absorbed customer is kept and marked, its shipments and recall rows move to the other, and both histories record it. A shipment of a product not yet on the market — at the concept, design, verification or pre-market stage, or at a stage not recorded — is recorded only with the reason it is leaving: clinical investigation, samples, prototype or pre-market evaluation, or another reason of at least twenty characters. It is not refused for the stage; it is refused without the reason, by the server whatever the form showed, and the product's stage, the reason and what was written are kept on the shipment and its audit event and shown in words; the database refuses a shipment record that breaks the same rule, counting the written reason in characters as the server does. A shipment of a product on the market or at end of life is asked nothing more. A customer may be recorded as the organisation's importer or distributor (EU MDR Articles 13 and 14), with the devices and markets it handles, the arrangement's dates and an optional link to the agreement; an SRN is accepted for an importer only, and one not in the form EUDAMED issues an importer's is pointed out, not refused. Only Super Admin, QA Admin and RA may record or archive one, since it decides whom a corrective action is told of. A shipment to that customer records the role the arrangement gave on the day it was made and keeps it after the arrangement ends; a shipment made before roles were recorded says the role was not on record rather than guessing it. A recall drawn from its shipments names it as such; the recall page also lists the importers and distributors its list does not name, for a person to weigh, and adds none. The importer's and distributor's own verifications (Articles 13(2) and 14(2)) are shown as what they must check, never as items the manufacturer ticks, quoted from the Official Journal in English, Danish and Swedish; the Norwegian is labelled an unofficial translation. An ended arrangement is archived with a reason of at least twenty characters and kept, and a live importer or distributor cannot be merged into another customer.
traceability-rules.test.ts:
- an implantable does
- anything supplied sterile does
- a single-use device does
- a reusable, non-sterile, non-implantable device does not
- refuses a sterile implantable with no lot and no serial
- accepts it with a lot
- accepts it with a serial and no lot
- treats whitespace as no identifier at all
- allows a template proof with no identifier — it is not for shipment
- does not require an identifier for a plain reusable device
- refuses an expiry before the manufacture date
- refuses a manufacture date in the future
- refuses an expiry that has already passed
- allows a reprint of a label whose stock has expired
- says nothing about dates that were not entered
- refuses the receipt the tester made: manufactured Aug 2026, expiring Jan 2026
- refuses equal dates — a device that expires the moment it is made
- refuses the shipment the tester made: no batch, no lot, no serial
- accepts a shipment with the batch linked
- refuses a traceable device on a lot alone, however plausible the lot looks
- accepts a lot alone for a device that needs no production identifier
- requires an identifier even for a device with no special characteristics
- refuses a Class IIb EU label with no notified-body number
- allows Class I, which is self-declared
- allows a US-only label — the CE mark is not printed there
- applies to a MULTI-market label, which carries the CE mark
- allows it once the number is there
- says nothing about a product that has not been classified yet
- is printed only when the device is prescription-only
- is not printed on an EU-only label
- pads a 13-digit code to 14, as Application Identifier (01) requires
- leaves a 14-digit code alone, so both modules agree
- ignores separators a person may type
the-carrier-is-a-carrier.test.ts:
- is drawn for a payload that has a device identifier
- is square, which is the shape nothing had to force
- encodes the fields it was given, in the GS1 element string
- carries the same fields as the linear barcode beside it
- changes when the data changes, and not otherwise
- refuses a payload with nothing in it
- refuses a lot with no device identifier to qualify it
- refuses a device identifier that is not a GTIN
- never throws, whatever it is handed
- the print view draws the symbol, on every label size
- issuing a label refuses a carrier that cannot be encoded
- and the scan verification that no test can do is written down
- refuses a traceable device on a typed lot alone (NV-121)
- accepts it once the batch is linked, however it was linked
- leaves a device that needs no production identifier as it was
- resolves the batch from the stock lot, and records it on the dispatch
- refuses in the order that tells the user what is actually wrong
- received stock has a way to be linked, or the gate is a wall
- the labels page does not describe a carrier the product now prints
register-recall-dispatch-labels.test.ts:
- refuses a lot a CAPA holds when Product is left 'Not linked', and writes nothing
- refuses a quarantined lot with no product, on its status alone
- refuses a shipment that names neither a product nor a batch, in plain words
- refuses when Stock cannot be read, rather than reading the silence as 'no such lot'
- still ships a released, unheld lot of a chosen product — the gate is not a wall
- recordDispatch is the only writer of a dispatch record
- the production page's Print labels refuses a provisional class under a CE mark
- and a Class IIb label with no notified-body number
- and an implantable batch with no lot
- and a template whose only identifier is the Basic UDI-DI — the carrier never encodes that
- issues the labels when everything is in order
- every function that creates a label generation calls the one gate first, and writes what it returned
- the gate decides by behaviour: a TEST proof may carry an unencodable carrier, a STOCK run may not
- the carrier is built from the UDI-DI only, as the linear barcode is, and printed as issued
- the validation pack no longer says the DataMatrix is not rendered
- refuses to issue a notice to a list on which everybody has been excluded
- counts only the consignees still in the recall, in the record and its sentence
- will not record a response that would bring an excluded consignee back
- offers Update and Exclude only to those the actions allow
- reads every lot in a list that repeats its keyword
- does not report every CAPA lot missing from a recall bounded only by dates
- links a recall to its CAPA afterwards, and a change says why on the record
- refuses a link from a role that may not manage the recall
- offers the CAPA on the new-recall form, on the recall page and from the CAPA
- does not treat a scope of unread keys as a product-wide recall, and names the keys
- refuses to open a recall whose scope names only keys nothing reads
- examines shipments recorded with no product whose lot the recall names, and says the ceiling bit
- puts truncation and a possible duplicate on the recall page
- flags stock lots RECALLED from the scope as the pull reads it — the lot key, any case
recall-chain.integration.test.ts (against a real database, as a signed-in user):
- refuses the shipment the tester made — no batch, no lot, no serial
- accepts it with the batch linked, and the record carries the identifier
- records why stock was not deducted, rather than saying nothing
- quarantines material from a supplier who is not approved, even in a cleared location
- says in the audit trail why it was quarantined
- refuses a lot that expires before it was made
- refuses a lot that is not in Stock, and records nothing
- matches the lot with stray spaces removed — and then applies the lot's own gate
- refuses a traceable device on a lot with no batch behind it
- accepts it once the stock lot is linked, without the form naming the batch
- still applies the release gate to a batch reached through the stock lot
- with the lot and product left empty, the record carries the batch's lot and product
- refuses a lot that contradicts the batch's, and records nothing
- refuses a product that is not the batch's
- refuses a batch with no lot recorded when no lot or serial is typed
- accepts that batch with the lot typed from the label
- puts in the recalled lot, leaves out the other, and flags the shipment with no lot
- records what it left out, not only what it took
- adds nothing a second time, and still says why
- a recall that names no lot reaches every shipment of the product
- keeps every contact, not only the last one
- refuses to take somebody off the list without a reason
- takes them off with the reason recorded, and keeps the row
- refuses to remove somebody who has already answered
- refuses a second exclusion of the same consignee
- refuses a batch whose record shows no material, under the batch's row lock, and signs nothing (B-28)
- refuses a release with no signature at all
- refuses a release with no reason, even where the identity is proven
- releases on a proven identity, and leaves a signature bound to the batch
the-product-says-whether-it-is-sterile.test.ts:
- a product recorded as supplied sterile needs a lot, and says so
- whatever the template says: the gate has no template flags to read
- not recorded is cautious: a lot is needed, and the refusal does not call the device sterile
- one fact unrecorded is still cautious, and names only that fact
- recorded not sterile and reusable relaxes as before: no lot needed for a non-implantable
- a test proof is still exempt
- a product recorded as sterile ships only against its batch
- not recorded is cautious, in the words O-26 chose
- recorded single use, sterility unrecorded: the recorded fact is claimed, the other is not
- recorded not sterile and reusable ships on a typed lot, as before
- only a product recorded sterile prints a sterile symbol, for a method that has one
- the single-use symbol only for a product recorded single use
- the facts are said in words, never as a stored code
- the template and the print view read the product, and no longer carry their own
- the schema keeps the template's columns only as the expand half, out of the client
- not recorded is stored as not recorded, and a method needs a sterile device
- refuses a sterile device with no method, or one the label does not know
- a family is sterile when any member is, not sterile only when every member says so
- the wizard's answer may not contradict what a product records
- a template still saying 'not sterile' cannot switch the lot off for a product recorded sterile
- a product recording nothing is read cautiously
- a product recorded not sterile and reusable issues without a lot, whatever the template once said
- adds nullable columns and copies each fact where every template agrees
the-product-says-whether-it-is-sterile.integration.test.ts (against a real database, as a signed-in user):
- the template's own 'not sterile' does not let a label go without a lot
- nor a shipment on a typed lot with no batch
- refuses a sterile device with no method, and changes nothing
- records supplied sterile by ethylene oxide, reusable, with the previous values on the trail
- a label run with no lot is refused for being supplied sterile
- a shipment on a typed lot is refused for being supplied sterile
- saving the template with its old flags unticked changes neither the product nor the gate
- the classification wizard may not answer 'not sterile' for it
- issues a label and records a shipment on a typed lot
- takes sterile, method and single use from the template, and leaves what it cannot trust as not recorded
- writes 'not supplied sterile' for a 'No', on the trail, and EUDAMED's B18 follows
- does not write a 'Yes', which needs a sterilisation method
- carries nothing over from a form that sends no answer for it
- never overwrites what the product records
the-label-prints-as-issued.test.ts:
- records the device, market, notified body, class, UDI, both carriers, symbols, manufacturer and size
- records the carrier's failure, not a placeholder, where it cannot be encoded — a UDI-DI with a bad check digit
- prints the issued UDI-DI, market and notified body after the template has been edited
- refuses a run with no snapshot — issued before 2026-10-04, or by the seed — and says how to issue a fresh one
- refuses a snapshot it cannot read rather than printing half of it
- refuses a run today's gate would refuse: the class has since become provisional under the CE mark
- refuses a seeded-style run whose UDI-DI never encoded, unless it is a proof
- refuses a stock run whose expiry has since passed
- marks a test proof as one; a stock run is not
- honours a qualified person's confirmation of a class the engine left provisional
- generateLabel writes what the labels carry onto the run
- the production page's Print labels does too
- a qualified person's confirmation unblocks a CE label, as the refusal promises — the gate reads it
- each change since issue has a message with the helper's English
- each purpose reads as labelPurposeInWords says it
- prints the issued values after the template's UDI-DI, market and notified body were changed
- marks every label of a test proof
- draws no label and no Print button for a run with no snapshot
- the demo UDI-DI carries its GS1 check digit
- the traceability seed creates no label run — a run is issued through the gate or not at all
- no longer claims a marking the print view never drew
the-label-prints-as-issued.integration.test.ts (against a real database, as a signed-in user):
- issues a CE label on a confirmed class, and records what it prints on the run
- after the template's UDI-DI, market and notified body are edited, the print model still shows the issued values
- a run with no snapshot — as the demo seed made — is not printable
- once the confirmation is withdrawn, the issued CE run is refused as a new one would be
customer-identity.test.ts:
- lower-cases, collapses spacing and trims
- keeps punctuation: 'St. Mary's' and 'St Mary's' are for a person to merge, not for the rule
- two shipments to one hospital, typed differently, are one customer
- the same name at another address is another customer — never merged automatically
- the country is part of it, and an empty country is no country
- the parts cannot run into each other: a line break is spacing, so it cannot be inside one
- its function is lower(btrim(regexp_replace(…, class, ' ', 'g'), ' ')) with the module's class
- and gives the same answer on every example
- keys a shipment as the code keys a consignee: name, address, country, one per line
- stores the country as the code does, and never merges two addresses
- numbers deterministically, past 9999 without cutting a digit
- links recall rows only through their shipment, and isolates the table like every tenant table
- can be applied over a partial run: everything is created if missing, and only empty links are filled
- the merge reason has the same floor in the database as in the code
- are CUS-0001 onwards, one past the highest in use, never counted from rows alone
- the first shipment makes CUS-0001; a second to the same hospital, typed differently, uses it
- the same name at another address is a new customer with the next number
- another organisation's identical customer is not this one's
- a consignee spelled as a merged customer was resolves to the customer it was merged into
- two shipments racing to create one hospital end on one customer
- a number taken a moment ago is stepped past
- into itself
- a customer already merged
- into a merged customer
- across organisations, or with one missing
- without a reason of at least twenty characters
- and an edit needs a reason and a customer that has not been merged
- two shipments to one hospital are one recipient with two shipments: 48 + 48 under one heading
- two hospitals with one name are two customers, told apart by number and address
- rows with no customer are kept together at the end, never guessed into a group by name
- an excluded shipment stays under its customer but out of the count
a-shipment-goes-to-a-customer.test.ts:
- a consignee no customer has yet becomes CUS-0001, and the shipment names it
- a consignee a customer already has, typed differently, is that customer — nothing is made
- a chosen customer fills what was left empty, and is linked as chosen
- a chosen customer that was merged is read as the customer it was merged into
- a customer id that names nothing here is refused, and nothing is written
- a shipment that is refused leaves no customer behind
- the customer is resolved after every refusal in the action, and just before the record
- moves the absorbed one's shipments and recall rows, marks it, and deletes nothing
- refuses a short reason, itself, and a role without the permission — before writing anything
- is QA's, RA's and the admins' — Manufacturing corrects a customer but does not merge (O-29)
- when somebody merged it a moment ago, says so and moves nothing
- needs a reason, writes only the customer, and keeps the previous values on the trail
- refuses a name, address and country another customer already has, and points at merging
- refuses a merged customer, and one changed by somebody else meanwhile
- the recall list is grouped by customer, says so in words, and lists unlinked rows as such
- a shipment's page links its customer and keeps the consignee as shipped
- the customer's page says where a merged customer went, and offers merge and correction with a reason
- the list is in the navigation, beside dispatch
- the assistant is told the customer by number and name — never a contact address or notes
a-shipment-before-the-market-says-why.test.ts:
- on market and end of life ship as before; every stage before the market, and a stage nobody knows, ask why
- counts characters as Postgres does, not UTF-16 units
- says a stage as the product pages say it
- refuses with no reason, an unknown reason, and an 'other' too short to say anything
- accepts a reason, keeps what was written trimmed, and drops a reason sent for an on-market product
- says a reason in words, never as stored
- refuses a shipment of a design-stage product with no reason, and writes nothing
- records it with a reason: on the shipment and on the audit event, in words
- 'Other' needs at least twenty characters, on the server whatever the form did
- a reason that is not one of those offered is refused
- ships a product at end of life as before: nothing asked, nothing written (not part of O-8)
- asks of a product whose stage it does not know, and the refusal says the stage in words
- reads the stage of the product a linked batch made, when no product was chosen
- is asked last: a shipment another rule refuses is refused for that, not for a missing reason
- ships an on-market product exactly as before: nothing asked, nothing extra written
- does not keep a reason the form sent for an on-market product
- the database's check offers the reasons the form offers, and the stages that ship without one
- every comparison in the check is guarded, so a NULL cannot pass it
- the schema has the three columns, all optional
- the shipment page and the list say the reason through the words function, never the stored value
- the form asks for the reason, for the product chosen or the batch's product
- is prose on the trail: an address in it is redacted, not refused after the shipment was written
- is held once, in the payload; the trail's sentence names the reason only
a-shipment-before-the-market-says-why.integration.test.ts (against a real database, as a signed-in user):
- refuses a stage that is not on the market with no reason — the NULL a check would let through
- refuses a reason on a stage that ships without one, and a reason the form does not offer
- counts 'other' in characters, as the server does
- accepts what the server writes: nothing, a reason, and a stage the code does not know
- ten emoji are refused by the server in words, before any customer or number is spent
- twenty emoji pass the server and the database alike
- an end-of-life product ships as before, with nothing asked and nothing written
customers.integration.test.ts (against a real database, as a signed-in user):
- two shipments to one hospital, typed differently, are one customer
- the same name at another address is another customer, with its own number
- a shipment to a chosen customer is linked to it and keeps what was typed for this shipment
- a customer of another organisation cannot be chosen
- rows drawn from the shipments carry their customers: two recipients, not four rows
- a shipment the old release wrote without a customer is given one when it is drawn in
- a merge with no reason is refused, and nothing moves
- a merge into itself is refused
- with a reason, the links move, the shipments stay as shipped, and nothing is deleted
- a merged customer cannot be merged again, and a shipment spelled its way goes to the survivor
- correcting a customer needs a reason and never rewrites a shipment
- a customer the database stamped, as the migration's were, can be corrected
- gives the code's answer on every example
- the customer table is isolated: with no organisation set, the application role sees none of it
importers-and-distributors.test.ts:
- is an importer or a distributor, and nothing else
- a customer recorded as one is live until its arrangement is archived
- its arrangement's standing follows its dates, the last day inside
- the markets are two-letter codes, said once each, sorted
- an agreement link is a web address or a page here, never a script
- is an importer's only: Article 31 gives a distributor none
- is within the EUDAMED tracker's bounds
- one not shaped as EUDAMED issues an importer's is said, not refused
- its customer, when that customer is a live importer or distributor
- and not a hospital, an unlinked shipment or an archived arrangement
- a new shipment records the role the arrangement gives on its day, and none outside the arrangement
- a shipment's page says the role the shipment recorded, whatever the customer is now
- records an existing customer as the importer, with its SRN and markets, on the trail
- creates a new customer for a distributor named on the register
- refuses before creating anything when the arrangement is refused
- a change to a live arrangement needs a reason, and keeps what it was
- refuses a merged customer: the arrangement belongs on the one it was merged into
- only in its own organisation: another tenant's customer is not found, and is not touched
- only with economic-operators:manage: Super Admin, QA Admin and RA, not every customer editor
- archives it with who, when and why — never deletes it
- a live importer or distributor is not merged away
- ${name}: the English catalogue says what the code says alone
- ${name}: Danish, Norwegian and Swedish say it in their own words
- every check and duty the page lists has words in each language
- Article 13(2) is quoted as the regulation says it: CE marking and DoC, the manufacturer and its AR, labelling and IFU, UDI
- the Danish quotes are the Official Journal's, with its defined term autoriseret repræsentant (Art. 2(32))
- the Swedish quotes are the Official Journal's
- the Norwegian is not passed off as a quote: no quotation marks, and labelled an unofficial translation
importers-and-distributors.integration.test.ts (against a real database, as a signed-in user):
- a distributor holds no SRN, and a role is one of the two
- records it with its SRN and markets, and writes the trail
- another organisation's customer cannot be recorded
- the shipment names the importer it went to (Article 25(2)(a))
- a shipment written before roles were recorded keeps none, and says so
- a recall drawn from that shipment reaches the importer
- archives it, keeps it, and the shipment no longer names a live operator
What they do not cover — and so what your PQ must. Whether the lot number entered is the lot that physically left, and whether the device characteristics recorded on the product match the device, are yours. The platform refuses a shipment that carries no identifier; it cannot tell a correct identifier from a plausible one. The two-dimensional GS1 DataMatrix carrier is rendered on every label size by a reference encoder, but no symbol has been read by a scanner: whether it scans on your printer, stock and label size is settled by a first-article print and scan grading (ISO/IEC 15415) before labels go on product, which is yours to perform and record.
M-PROD — Product register and market release
Your obligation: EU MDR Article 19 · EU MDR Article 27 · EU MDR Annex IV · ISO 13485 §7.5.9
What our tests establish. A UDI-DI is unique within the organisation and, for a GS1 identifier, has a valid check digit; a product cannot take a sibling's UDI-DI. A device cannot be set ON MARKET without an effective Declaration of Conformity on the product or its family, under any market's declaration type, unless a written justification is recorded; PRE MARKET is not gated. Every stage change is on the audit trail with the actor, the previous stage and the justification.
product-udi.integration.test.ts (against a real database, as a signed-in user):
- refuses a new product whose UDI-DI another product in the organisation already carries, naming it
- refuses editing a product's UDI-DI to a sibling's value
- lets a product keep its own UDI-DI on edit
- refuses an invalid GS1 check digit and says what it should be
- accepts a valid new GTIN, one held by another organisation, and an HIBCC identifier
product-release-gates.integration.test.ts (against a real database, as a signed-in user):
- PRE_MARKET is not gated: conformity assessment happens before the declaration exists
- ON_MARKET with no declaration and no justification is refused, and nothing changes
- a declaration still in draft does not count
- a justification of substance releases the device, and the reason and the actor are on the trail
- 'ok' is not a justification
- an approved declaration is not yet issued, so it does not count (2026-10-23)
- an effective declaration on the product releases it without a justification
- an effective declaration on the family covers every member (the dossier is the family's)
- a market-specific declaration counts too
What they do not cover — and so what your PQ must. Whether the declaration is correct, signed by the right person and covers the device as configured is yours. The justification path exists because release may be controlled under your own procedure; the platform records that you said so, it does not check it. Devices brought in by bulk import are recorded at the stage you give them, not gated.
QMSN-MEA-001 · v1.5.0 · pack rev. J (2026-09-07)
Published at qmsnordic.com/legal/validation/module-evidence. Cite the version above in your validation record — a pack is evidence, and evidence has to stay retrievable at the version you validated against.
