<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: NarcCount</title>
    <description>The latest articles on DEV Community by NarcCount (@narccount).</description>
    <link>https://dev.to/narccount</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4207344%2Fc604c234-e9cf-4888-85b3-c7c51447c00a.png</url>
      <title>DEV Community: NarcCount</title>
      <link>https://dev.to/narccount</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/narccount"/>
    <language>en</language>
    <item>
      <title>The arithmetic behind a pharmacy narcotic count, and why the schedule depends on the province</title>
      <dc:creator>NarcCount</dc:creator>
      <pubDate>Sun, 25 Oct 2026 21:22:29 +0000</pubDate>
      <link>https://dev.to/narccount/the-arithmetic-behind-a-pharmacy-narcotic-count-and-why-the-schedule-depends-on-the-province-4ckg</link>
      <guid>https://dev.to/narccount/the-arithmetic-behind-a-pharmacy-narcotic-count-and-why-the-schedule-depends-on-the-province-4ckg</guid>
      <description>&lt;p&gt;NarcCount makes narcotic reconciliation software for Canadian pharmacies. This post is not a product tour. It walks through the arithmetic every controlled-substance reconciliation rests on, the data problems that make it harder than it looks, and the one parameter that changes from province to province: how often the count has to happen. If you build inventory, health or compliance software, the shape of the problem is worth knowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  One line of arithmetic
&lt;/h2&gt;

&lt;p&gt;For each drug and strength:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expected = last_count + received + returned_to_stock - dispensed - destroyed
variance = counted - expected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A variance of zero means the shelf matches the records. Everything else in a reconciliation, from the log format to the investigation and the signed record, exists to make sure each of those inputs is complete and dated correctly.&lt;/p&gt;

&lt;p&gt;A worked example. One drug and strength has an opening balance of 48 tablets. A wholesaler order adds 100. Dispenses over the period total 56. Six expired tablets are destroyed and recorded. Six tablets come back to stock from a prescription that was filled but never picked up, after the claim is reversed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;48 + 100 + 6 - 56 - 6 = 92
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A physical count of 92 agrees. Had the unclaimed prescription gone back on the shelf without the dispense being reversed, the count would still find 92 but the records would expect 86: an overage of 6 with no obvious cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  The window is the subtle part
&lt;/h2&gt;

&lt;p&gt;"Last count" means the most recent prior count that included &lt;em&gt;this drug&lt;/em&gt;, not the last count the pharmacy did. Receipts, dispenses and destructions are summed over the window that runs from the day after that prior count up to and including the current count date. A drug that has never been counted has no baseline, so its first count simply sets the opening balance and reports no variance.&lt;/p&gt;

&lt;p&gt;Here is a minimal sketch of that logic in TypeScript. It is an illustration of the rule, not production code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;received&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;returned&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dispensed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destroyed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Txn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;drugId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Kind&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;drugId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;counted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Dates are ISO strings (YYYY-MM-DD), so string comparison orders them correctly.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;priorCounts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;txns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Txn&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prior&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;priorCounts&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drugId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drugId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="c1"&gt;// First count of this drug: it becomes the opening balance, no variance.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;prior&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;variance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inWindow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;txns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drugId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drugId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Kind&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;inWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;counted&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;received&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;returned&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dispensed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destroyed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;variance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;counted&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why balances drift
&lt;/h2&gt;

&lt;p&gt;The formula is trivial. The data feeding it is not. When a count and the records disagree, the cause is usually one of a short list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part fills recorded as the full quantity, or the balance of a part fill dispensed without its own entry&lt;/li&gt;
&lt;li&gt;Unclaimed prescriptions returned to the shelf without reversing the dispense&lt;/li&gt;
&lt;li&gt;Patient returns added back into stock instead of being set aside for destruction&lt;/li&gt;
&lt;li&gt;A dispense or receipt recorded under an interchangeable brand's DIN, so one product's balance runs short and the other's runs over&lt;/li&gt;
&lt;li&gt;Transactions dated just before or after the count cut-off&lt;/li&gt;
&lt;li&gt;Destructions carried out but not entered, which show as a shortage&lt;/li&gt;
&lt;li&gt;Unit mismatches, such as a box of 5 patches received and entered as 1&lt;/li&gt;
&lt;li&gt;Measuring variance on liquids, which produces small, steady differences rather than whole units&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two of those are data-modelling problems more than counting problems: product identity (which DIN a transaction belongs to) and units (a pack versus the units inside it). Expected on-hand is only as accurate as the transaction records behind it, so an import that gets identity or units wrong produces a variance that nobody on the shelf can explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Corrections are rows, not edits
&lt;/h2&gt;

&lt;p&gt;A balance that can be overwritten is a balance with no history. The habits that keep a record trustworthy are the same whether it lives in a spreadsheet or a database: add rows for new transactions instead of editing old ones, record corrections as adjusting rows with a reason, and keep a dated, read-only copy of every count so that record cannot change afterward.&lt;/p&gt;

&lt;p&gt;When an investigation explains a variance, the fix is to add the missing or corrected transaction, for example the unentered destruction, so the balance becomes right for a traceable reason. When no cause is found, an adjusting entry that brings the balance to the counted quantity, with a note linking it to the investigation, keeps the evidence. Overwriting the balance to match the shelf erases it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parameter that changes by province
&lt;/h2&gt;

&lt;p&gt;Canada has no single national schedule for counting narcotics and controlled drugs in a pharmacy. Each provincial regulator sets its own interval, and that interval is a minimum rather than a timetable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nova Scotia:&lt;/strong&gt; the physical count and its reconciliation are completed and documented monthly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prince Edward Island:&lt;/strong&gt; narcotic and controlled drugs are counted monthly; targeted substances are reconciled at least once every six months.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alberta, British Columbia, Saskatchewan, Manitoba, and Newfoundland and Labrador:&lt;/strong&gt; reconciliation is generally required every three months. Alberta's standard also calls for an audit of the perpetual inventory each time there is a change in proprietor or licensee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ontario:&lt;/strong&gt; the Ontario College of Pharmacists expects narcotics and controlled substances to be reconciled at least every six months.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A pharmacy group with locations in two provinces therefore runs two different intervals, and each location follows its own college. In a data model, the interval belongs to the location, not to the account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why many pharmacies count more often than the minimum
&lt;/h2&gt;

&lt;p&gt;The difficulty of explaining a variance grows with the number of transactions behind it. Take a pharmacy that dispenses around 40 prescriptions of hydromorphone 2 mg between monthly counts. A shortage of 6 tablets on a monthly count means reviewing about 40 dispenses, a handful of receipts and any destructions. The same shortage found on a count at the Ontario minimum means reviewing roughly six times as many entries. That is why fast-moving products often get spot counts between full counts, while slow-moving items follow the full schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;The intervals above come from each province's college; regulations change, so confirm the current requirement with the source before relying on it. The province-by-province breakdown with its primary sources is in our guide to &lt;a href="https://narccount.ca/resources/controlled-substance-inventory-frequency-canada" rel="noopener noreferrer"&gt;controlled substance inventory frequency in Canada&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;General information, not legal or professional advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>healthcare</category>
      <category>database</category>
      <category>typescript</category>
      <category>canada</category>
    </item>
  </channel>
</rss>
