endear/oe-module-endear-dscsa

Endear DSCSA Module — DSCSA serialized inventory, inbound scan/verify, and dispense scan-out for OpenEMR. Connects to Endear's EPCIS/AS2 service.

Maintainers

Package info

github.com/endearai/oe-module-endear-dscsa

Homepage

Issues

Type:openemr-module

pkg:composer/endear/oe-module-endear-dscsa

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

1.7.0 2026-08-03 21:47 UTC

This package is auto-updated.

Last update: 2026-08-03 23:59:22 UTC


README

DSCSA serialized-inventory integration for OpenEMR 8.0.1. Pulls serialized units from the Endear DSCSA Module API (dispense.endear.com), files them against OpenEMR's in-house pharmacy inventory, and provides inbound scan/verify and dispense scan-out with point-of-care verification.

This module holds only the operational subset needed to dispense. Your Postgres (AWS RDS) behind the API remains the DSCSA compliance system of record.

License

The code in this repository is licensed under GPL-3.0-or-later (see LICENSE). There are no license keys, obfuscated code, or disabled features hidden in this module — everything here is inspectable, forkable, and free to modify under GPL terms.

This module is a connector, not the service it connects to. It talks to Endear's hosted AS2/EPCIS compliance service (dispense.endear.com) over the documented REST API in BILLING-API.md and the other *-API.md files. That hosted service is not part of this repository, is not open source, and is not free.

Free / GPL (this repo) The OpenEMR-side scan-in, scan-out, inventory, DEA/PDMP, and billing UI, plus the API client that talks to Endear's service.
Paid, separate from this repo The Endear DSCSA+ hosted service — EPCIS archive, AS2 transport, entitlement/billing. Requires an active subscription: see endear.com/pricing or the Billing tab inside the module.

Unlike some ePrescribing modules in this ecosystem, there is currently no free tier of the hosted service — using this module for DSCSA/DSCSA+ inventory sync requires a paid Endear subscription. Reading, forking, or building your own connector against the documented API does not.

This module is developed and maintained by Endear Inc. and is not officially affiliated with, or endorsed by, the OpenEMR Foundation.

DSCSA vs. DSCSA+

One module, one package name — the deeper controlled-substance features sit behind an Endear subscription tier, not a separate install:

DSCSA (base) DSCSA+
Serialized inventory (scan-in/out, verify, dispense)
GTIN↔NDC mapping, sync, returns, investigations
DEA schedule delineation (dea_schedule on every serial)
Named 21 CFR 1304.04 dispensing record (endear_dea_dispense_record)
State controlled-substance add-ons (e.g. MA PDMP/MassPAT)

CSA (Endear's SharePoint/SPFx compliance app) is not bundled here — it's a separate product a practice purchases on its own. What this module provides either way is the correlation_token: a random, information-free id persisted on every controlled-substance dispense, safe to hand to CSA or any external compliance system without exposing patient identity. DSCSA+ is what makes that token worth having — it's the tier that actually populates a named record behind it (patient/prescriber/PDMP detail, all kept local to OpenEMR).

Plan-tier enforcement lives server-side (GET /billing/plans, GET /billing/subscription, see Subscription & billing below) — this client stays the same GPL code regardless of tier; what dispense-api accepts back is what actually gates it.

State coverage today: Massachusetts only. PdmpPolicy and the PDMP Reports screen are hardcoded to MA's schedule list and MassPAT/PMP AWARxE terminology. A practice in another state can still use DSCSA+ for DEA recordkeeping, but the PDMP draft feature isn't yet state-configurable — treat it as "MassPAT support," not generic PDMP support, until that's split out.

How it fits together

Trading partners ──AS2──▶ Endear API (dispense.endear.com) ──REST──▶ this module ──▶ OpenEMR inventory
                                                                          │
                                                  scan-in (+on_hand)  ◀───┤
                                                  scan-out (−on_hand) ────┘──POST /dispense──▶ Endear

The module never alters core OpenEMR tables. It adds tables and links to drugs / drug_inventory:

DSCSA (base):

  • endear_dispense_inventory_serial — one row per serialized unit (the serial layer core OpenEMR lacks)
  • endear_dispense_gtin_ndc_map — GTIN → NDC → drugs.drug_id crosswalk
  • endear_dispense_sync_log — audit of each pull cycle
  • endear_dispense_external, endear_dispense_removal, endear_dispense_investigation — 3PL dispensing, returns/destruction, and investigation workflows

DSCSA+:

  • endear_dea_dispense_record — the named 21 CFR 1304.04 dispensing record (patient/prescriber/schedule). Never transmitted; only its correlation_token is shared externally.
  • endear_pdmp_report — auto-generated PDMP draft for a take-home dispense of a reportable schedule; holds the clean summary staff key into the state portal (MassPAT/PMP AWARxE today), plus who/when/reference once marked submitted.

Install

Option A — Composer (recommended once published on Packagist):

composer require endear/oe-module-endear-dscsa

The openemr/oe-module-installer-plugin dependency places it at interface/modules/custom_modules/oe-module-endear-dscsa/. Then run composer dump-autoload from your OpenEMR root so the namespace registers.

Option B — Manual (today's actual path — not yet on Packagist): download the release zip and extract it into <openemr>/interface/modules/custom_modules/oe-module-endear-dscsa/, then run the bundled fix-up script once, before Register/Install/Enable:

cd <openemr>/interface/modules/custom_modules/oe-module-endear-dscsa/
sh install.sh

Run this as root (or whoever's shell account owns the files you just copied in) — not as an OpenEMR login, and never as the web server's own account (apache/www-data). This is an OS-level shell command on the server/container, unrelated to logging into OpenEMR itself; in a typical Docker deployment that means docker exec -it <container> sh as root, cd into the module directory, then run the script. It specifically can't run as the web server's account, since that account not yet being able to read these files is the exact problem it fixes — only the file owner or root can grant that. Once this script has run, Register/Install/Enable in the OpenEMR web UI is the normal admin-user step, same as any other module.

install.sh does two things, both of which matter regardless of which one bites you first:

  1. Permissions. Whatever copied the folder in (docker cp, a zip extract, scp) almost always leaves it owned by root or your own host user, not the user the web server runs as. OpenEMR's module manager checks is_readable() on the module's openemr.bootstrap.php on every request; if the web server user can't read it, OpenEMR retries 3 times, logs Custom module bootstrap file ... is not readable, then silently force-disables the module — Enable appears to work for a moment and then reverts, with no error shown in the UI. The script fixes this with chmod -R a+rX, which works regardless of which account the web server actually runs as (apache, www-data, a php-fpm pool user — it varies by image), so there's nothing to detect.
  2. Dependencies, but only if vendor/ isn't already there. Release zips ship it pre-bundled, so this is normally a no-op; it only runs composer install --no-dev if you're working from a source checkout instead. The module's own fallback autoloader only covers its own classes, not the one real third-party dependency (firebase/php-jwt, used to verify signed DSCSA+ entitlement tokens) — skipping this when vendor/ really is missing doesn't break Enable, it fails later and silently, the first time a page calls EntitlementPolicy::hasDscsaPlus().

Neither step can be automated further than "run one script" — a Unix process can only chmod/chown files it already owns (or run as root), and the web server's own account is neither, since it didn't create these files. Some privileged step (root, or whoever owns the files) has to run once, no matter what. If you'd rather chown to a specific user than open permissions to everyone, that works too, in place of the script's chmod:

ps aux | grep -E 'apache|httpd|php-fpm'   # find the user Apache/PHP runs as
chown -R apache:apache <openemr>/interface/modules/custom_modules/oe-module-endear-dscsa

Then, either way, in OpenEMR: Modules → Manage Modules → Unregistered, Register "Endear DSCSA Module", then Install, then Enable. Install creates the schema and the background-service row from table.sql (idempotent). If your build doesn't auto-run it on enable:

mysql -u <user> -p <openemr_db> < table.sql

Troubleshooting

  • Enable reverts to Inactive with no visible error → ownership issue above. Check the web server's error log for oe-module-endear-dscsa — a real, literal message (... is not readable. Retry Count: N, then System will force disable module.) confirms it.
  • Any module page 500s immediately after Enable → check the same error log for the actual PHP exception. If it's ArgumentCountError on CsrfUtils::collectCsrfToken()/verifyCsrfToken(), you're on an older copy predating the OpenEMR 8 CSRF API fix in this module (fixed 07292026) — pull the current release.
  • composer install fails citing a security advisory on firebase/php-jwt → shouldn't happen on a current release (composer.json requires ^7.1, clear of the CVE-2025-45769 advisory affecting <7.0.0). If you see this, your copy's composer.json is stale; re-download the current release.

Publishing to the OpenEMR community

  1. Push to a public GitHub repo; tag a release (e.g. v0.2.0) matching the composer.json version.
  2. Submit the repo to packagist.org so users can composer require it. Enable the GitHub→Packagist webhook for auto-updates on new tags.
  3. Announce on the OpenEMR community forum (community.open-emr.org).

Note: third-party modules can't appear by default on the Manage Modules list the way Weno/Fax do — that requires inclusion in OpenEMR core. Packagist + composer is the distribution path.

Configure

Administration → Globals → Connectors → Endear DSCSA Module

Setting Notes
Environment prd or stg — must match the API key
API base URL blank = default (https://dispense.endear.com/api/v1 or …stg…)
Practice API key the X-Practice-API-Key issued by Endear (stored encrypted)
OpenEMR instance label sent as openemr_instance on dispense events
Pull look-back (days) window for /inventory/pending (default 30)
Transmit patient MRN OFF by default — see PHI note below
Reconcile dispense against native drug sales ON by default. Lets the standalone Dispense screen match a scanned serial to a recent native drug_sales row so the dispense carries patient/Rx linkage — read-only, writes nothing to native tables. One of two ways (see "Patient/Rx linkage" below) the standalone screen can supply the linked patient DSCSA+ requires for a take-home or outpatient-pickup dispense of a PDMP-reportable schedule.

The base URL + key + environment are a matched set: a staging key against the production URL returns 401 by design.

Patient/Rx linkage on the Dispense screen

This fork does not depend on any core-file patch. The standalone Dispense (Scan Out) screen has its own patient/prescription picker: search for a patient by name, then pick one of their prescriptions for the scanned drug — this ties the dispense to a real patient/Rx/prescriber without any pre-existing native drug_sales row. It coexists with the reconcile-against-recent-native-sale picker (endear_dispense_reconcile, ON by default); either one, both, or neither (typed Rx#/encounter only) all work. For DSCSA+ practices, at least one of these two linkage paths is required for any take-home or outpatient-pickup dispense of a PDMP-reportable schedule — that's the one case the code hard-blocks without a linked patient.

(07282026: this same picker also exists in Endear's internal OpenEHR-specific module, as an alternative alongside its own core-file-patch reconcile path — see that module's README for how the two installs differ.)

Permissions, menu, cron

  • ACL: every page is gated on the inventory / lots ACO. Grant it to the pharmacy roles under Administration → ACL.
  • Menu: appears under Modules → Endear DSCSA Module (Inventory, Scan In, Dispense, Internal, DEA Records, PDMP Reports, External (3PL), Investigate, Returns, Billing, Setup). DEA Records and PDMP Reports are DSCSA+ screens — DEA Records is named-record recordkeeping (21 CFR 1304.04); PDMP Reports is the draft/review queue for take-home dispenses of a reportable schedule. Both stay fully local to OpenEMR; nothing on either screen is transmitted.
  • Background sync: registers an OpenEMR background service (Endear_Dispense_Sync, every 15 min). It runs whenever run_background_services.php is invoked, so make sure OpenEMR's cron / scheduled task is active. You can also pull on demand with Sync now on the dashboard.

Drug mapping prerequisite

A serial can only be received once its GTIN maps to an OpenEMR drug. Resolution runs highest-confidence first:

  1. Crosswalk — a confirmed endear_dispense_gtin_ndc_map entry.
  2. Deterministic GTIN→NDC — for FDA-namespace GTINs (indicator + "03" + NDC10 + check), the native NDC10 is sliced from the GTIN and check-digit validated, then matched to the formulary. Stored NDC11 (5-4-2) values are matched by enumerating the ≤3 native configs (4-4-2 / 5-3-2 / 5-4-1). The module auto-maps only on a single unambiguous match — true one-to-one.
  3. Direct NDC — the NDC carried on the record vs drugs.ndc_number.

Anything ambiguous, or any GTIN that uses a manufacturer's own GS1 company prefix (no embedded NDC), is left unmapped for the Modules → Endear DSCSA Module → GTIN Mapping screen. There you can confirm a suggested one-to-one match, choose among ambiguous candidates, search the formulary by name/NDC, or run Auto-map resolvable to clear every unambiguous one in a batch.

Highest accuracy comes from seeding the crosswalk from the DSCSA app's FDA data (which knows each labeler's native segmentation authoritatively and can cover non-NDC GTINs). If Endear later exposes a master-data endpoint returning GTIN↔NDC↔drug, point the crosswalk at it; the deterministic local calc remains the offline fallback.

Endpoints consumed

Module action Endear endpoint
Background / Sync now GET /inventory/pending, then POST /inventory/acknowledge
Scan-in & scan-out verify GET /inventory/verify?serial=&gtin=
Dispense POST /inventory/dispense

Sync safety: pull → write locally → acknowledge only what committed. A crash mid-sync leaves rows un-acked; they reappear next cycle and de-duplicate on staging_id. The module does not rely on the server since cursor (it is currently ignored server-side) — it de-dups locally instead. If you later add since to the API's get_pending signature, this module is already safe to switch over.

PHI note

POST /inventory/dispense hashes patient MRN server-side, but sending the raw MRN is still a PHI transmission. The module therefore does not send MRN by default — it sends only rx_number and encounter_id for traceability. Turn on "Transmit patient MRN" only if your BAA with Endear covers it.

Subscription & billing

Modules → Endear DSCSA Module → Subscription & Billing shows the practice's current plan and quota, lists plans, and starts checkout. Design rules:

  • Prices are never hardcoded. The page renders whatever GET /billing/plans returns, so Endear controls pricing server-side (and this GPL code carries no price list).
  • No card data touches OpenEMR. "Subscribe" calls POST /billing/checkout, receives a processor-hosted URL (e.g. Stripe Checkout), and opens it in a new tab — payment happens entirely on the hosted page (keeps PCI scope off the EMR and off this module). "Manage billing" uses POST /billing/portal the same way.
  • The page degrades gracefully if the billing endpoints aren't live yet.

The backend contract Endear must implement is in BILLING-API.md.

File list

composer.json                  type: openemr-module + installer-plugin require
table.sql                      schema + background_services row (install)
LICENSE / CHANGELOG.md
openemr.bootstrap.php           module load entry
src/Bootstrap.php               globals, menu, ACL, install
src/Client/DispenseApiClient.php   inventory + billing REST calls
src/Lib/Gs1Parser.php           GS1 DataMatrix parser (AIs 01/17/10/21, FNC1)
src/Lib/GtinNdc.php             deterministic GTIN-14 <-> NDC conversion
src/Support/PdmpPolicy.php      DSCSA+: which DEA schedules require PDMP reporting (MA only today) + due-date calc
src/Service/SerialInventoryService.php   local DB, mapping, drug_inventory linkage, DEA record + PDMP draft persistence
src/Service/SyncService.php     one pull cycle (ack-after-commit)
src/Controller/DispenseController.php    page + JSON dispatch, incl. DEA Records / PDMP Reports
public/index.php                front controller
public/cron-sync.php            background-service hook
templates/*.html.twig           dashboard, scan-in, scan-out, mapping, billing,
                                 dea_dispense_records (DSCSA+), pdmp_review (DSCSA+)