Administration
Everything an administrator sets up once and rarely touches again: who can see and change what, the per-branch operating rules, the two advanced areas that stay off until validated, the data-privacy controls, the audit trail, the scheduled jobs and dashboard widgets, the read-only API surface, and the migration and seeding commands that stand a tenant up.
Menu group: Pharmacy | Enable flag: module_pharmacy = 1 | Sub-toggles: module_pharmacy_controlled, module_pharmacy_quality
Permissions & roles
Pharmacy contributes its own RBAC keys to the Roles editor while the module is enabled. The base pharmacy key is the module Enable toggle; the rest gate specific surfaces. Keys are serialized into a user's permissions by name, so they are frozen once shipped. Administrators bypass every check.
| Permission key | Grants | Suggested holders |
|---|---|---|
pharmacy | Base access and the Overview command center. Without it the Pharmacy menu never appears. | Every pharmacy staff member. |
pharmacy_compliance (+_create/_update/_delete) | The Compliance Center tabs — licenses, credentials, renewal tasks, inspection packs — and their CRUD. | Compliance officer, branch manager, PIC. |
pharmacy_duty | Start and end pharmacist duty sessions — the source of the on-duty gate. | Pharmacists, shift supervisors, the PIC. |
pharmacy_product (+_create/_update/_delete) | The Drug Catalog, generic groups, regulatory verifications and supplier licensing. | Pharmacist, inventory lead. |
pharmacy_expiry | The read-only Near-Expiry & Lots dashboard. | Pharmacist, inventory lead. |
pharmacy_prescription (+_create/_update/_delete) | View the Rx queue; capture, edit and delete prescriptions. | Assistants (capture), pharmacists. |
pharmacy_prescription_validate | Validate or reject a submitted prescription (the pharmacist's check). | Pharmacists only. |
pharmacy_dispense | Record dispensing fills against a validated Rx. | Pharmacists. |
pharmacy_patient (+_create/_update/_delete) | Patients & prescribers and their representatives / medication profile. | Front desk, pharmacists. |
pharmacy_patient_view_sensitive | Unmask a patient's full ID number (an audited action). | Pharmacist / PIC only. |
pharmacy_controlled_view / _post / _correct | View the controlled register; post entries and reconcile; post reversing corrections. (Needs the sub-toggle.) | Pharmacist-in-Charge. |
pharmacy_quality (+_create/_update/_delete) | Recalls, adverse events, cold-chain records and their CRUD. (Needs the sub-toggle.) | Quality lead, PIC. |
pharmacy_recall_manage | Activate / close a recall (which quarantines the batch). | PIC / owner. |
pharmacy_settings | Branch profiles and the advanced sub-toggles. | Owner / branch manager only. |
pharmacy_settings, pharmacy_patient_view_sensitive, pharmacy_compliance_delete, pharmacy_recall_manage and the controlled-register keys for owners/managers/PIC. Most counter staff need only a subset of the compliance, prescription and patient families.Branch profiles & operating rules
A branch profile (one per pharmacy location, created in Pharmacy → Settings) holds that store's operating profile and the rule flags the module reads. Multi-branch operators keep a separate profile per store; single-branch installs keep one. Every save is written to the audit trail.
| Flag | Default | What it does |
|---|---|---|
| Operating profile | Community drugstore | community_drugstore / non_prescription / institutional. |
| Deployment mode | Micro | micro / multi_branch / enterprise — a sizing hint. |
| Pharmacist-in-Charge | — | The staff user recorded as the branch PIC. |
| Require pharmacist on duty | On | The hard gate for regulated dispensing (see below). Leave on for a licensed drugstore. |
| FEFO | On | First-Expiry-First-Out handling of medicine stock — reuses Warehouse's lot/batch/expiry engine. |
| Senior Citizen booklet required | Off | Off by default per FDA Circular 2025-005. Turn on only if your policy still requires it. |
| PWD booklet required | Configurable | Per-branch to match your PWD-discount recording policy. |
| Controlled-drug register (branch flag) | Off | A per-branch marker; the register itself is gated by the module sub-toggle (below). |
| Prescription retention (years) | 5 (minimum enforced at 2) | How long dispensing records are kept. The statutory minimum is 2 years. |
| Expiry warning horizon (days) | 180 | How far ahead a license or credential is flagged as "expiring" on the Overview and in reminders. |
Advanced sub-modules (Settings → Advanced Modules)
Two regulated areas are gated by their own tenant settings, editable in Pharmacy → Settings → Advanced Modules (needs pharmacy_settings). Both default off. While a sub-toggle is off, its menu item is hidden and a direct URL to its controller is redirected — so the area can never be reached half-configured.
| Sub-module | Setting | Enables |
|---|---|---|
| Controlled-drug register | module_pharmacy_controlled | The append-only RA 9165 ledger, running balance, correction-by-reversal, and physical-count reconciliation. |
| Quality & Safety | module_pharmacy_quality | Recalls / stop-sale (batch quarantine), adverse events, and cold-chain temperature logging. |
Toggling either is itself written to the audit trail. Enable each only after your tenant validates the corresponding compliance procedure (PDEA / DDB for the controlled register; your pharmacovigilance and cold-chain SOPs for Quality & Safety).
The pharmacist-on-duty gate
A community pharmacy must have a credentialed pharmacist physically present whenever prescription medicines are dispensed. Pharmacy makes that a first-class, enforceable state rather than a paper log:
- Going on duty needs a valid credential. A pharmacist can only start a duty session if they have a non-expired PRC / PIC credential on file. The session records who, when, at which branch/counter, and which credential authorised it.
- The session is the gate. Whether a pharmacist is on duty is derived from these sessions, and it is the exact signal that prescription validation and dispensing block on when Require pharmacist on duty is enabled — validate and record-fill both refuse with a "pharmacist required" message when nobody is on duty.
- Every start/end is audited at warning severity, so the on-duty record is part of the permanent trail.
pharmacy_duty to the people who actually work the counter.Data privacy — patient IDs (RA 10173)
Retail patient records carry a government ID number (Senior / PWD / PhilHealth), which is personal data under the Data Privacy Act. Pharmacy handles it defensively:
- Encrypted at rest. The full ID number is stored as an AES-256-GCM envelope; only the last 4 digits are kept in the clear for display.
- Masked by default. Lists and the patient view show only a masked last-4 — never the full number.
- Permission-gated, audited unmasking. Reading the full number requires
pharmacy_patient_view_sensitiveand writes a "full patient ID was viewed" entry to the audit trail against the user who revealed it. - Edit-safe. The ID field is never pre-filled on the edit form; leaving it blank keeps the stored value, so an edit never accidentally overwrites or exposes it.
Grant pharmacy_patient_view_sensitive sparingly — to the pharmacist/PIC roles that genuinely need to verify an ID at the counter.
Audit trail (system_logs)
Every mutating action in the module writes a system_logs entry through registered action:component keys. These labels are merged into the core log config ungated by active state, so historical entries still resolve even if the module is later disabled. Filter the core Activity Logs by the Pharmacy category to review who changed what and when.
| Area | Logged actions | Notable severity |
|---|---|---|
| Licenses & Credentials | create · update · delete | critical on delete |
| Pharmacist Duty | session started / ended | warning |
| Renewal Tasks / Inspection Packs | create · update · delete / generate · delete | info |
| Branch profile / settings | create · update (incl. sub-toggle changes) | warning on update |
| Drug Catalog / Generic Groups / Suppliers / Verifications | create · update · delete | info |
| Patients | create · update · delete · view (unmasked ID) | warning on delete |
| Prescriptions / Dispensing | create · status (submit/validate/reject/cancel) · delete / fill created | warning on status |
| Statutory benefit | benefit decision applied | info |
| Controlled register | entry / correction / reconciliation posted | warning |
| Recalls / Adverse events | create · update (recall activate) | warning (recall) |
Deletes are soft deletes, so a removed record is still recoverable and still present in the trail. The controlled register additionally never deletes at all — a correction is a linked reversing entry.
Cron jobs (per-tenant, daily)
Two scheduled jobs run per tenant. Both self-gate on module_pharmacy (a disabled tenant is a clean no-op) and fire best-effort notifications; the near-expiry job also skips cleanly when the Warehouse tables are absent.
| Job (slug) | Schedule | What it does |
|---|---|---|
pharmacy_license_expiry | Daily, 07:00 | Flags FDA licenses, permits and pharmacist credentials expiring within 30 days (or already expired) and notifies. |
pharmacy_near_expiry_stock | Daily, 06:30 | Scans pharmacy-classified stock lots expiring within 90 days from the Warehouse batch ledger and notifies. |
They are discovered automatically by the cron runtime and appear in php erpat cron:list. Per-event notification templates and recipients are a follow-up; the jobs currently raise a best-effort notification.
Dashboard widgets
Two staff dashboard widgets ship with the module, both gated on module_pharmacy + the pharmacy_compliance permission and on by default:
- Compliance Readiness — the branch readiness score at a glance.
- Expiring Licenses & Credentials — what's lapsing soon.
Users add or remove them like any other dashboard widget; they render through the core Dashboard.
The dual read-only API
Pharmacy exposes two read-only API surfaces. Neither exposes patient, dispensing or controlled-register data. A tenant with the module disabled returns 404 module_unavailable (hiding the resource's existence) rather than a 403.
| Surface | Base path | Auth & gating | Endpoints |
|---|---|---|---|
| Integration API (client / machine-to-machine) | /api/v1/pharmacy |
Bearer token with the pharmacy:read scope. |
GET /products, GET /products/{id}, GET /compliance (aggregate readiness counts only) |
| End-User API (employee self-service) | /v1/api/pharmacy |
Authenticated employee, gated by module_pharmacy + the matching web permit (pharmacy_product / pharmacy_expiry). |
GET /products, GET /products/{id}, GET /near-expiry (needs the Warehouse WMS; returns an empty list cleanly without it) |
The pharmacy:read scope appears in the API client-management UI as "Pharmacy (Read)". Both surfaces are paginated (?page, ?per_page) and support a ?classification filter on the product list; near-expiry accepts ?within (days).
Migrations & the demo seeder
The module owns its schema and ships its slices as six timestamped migrations, tracked in the per-module migrations_pharmacy table (separate from the core migrations table). Run them with:
php erpat migrate:modules
- Compliance tables — branch profiles, licenses, credentials, duty sessions, compliance tasks, inspection packs.
- Catalog tables — product profiles, generic groups, regulatory verifications, supplier profiles.
- Prescription tables — patients, representatives, prescribers, prescriptions, items, documents, dispensing records and items.
- Benefit tables — benefit rule versions, decisions, and per-line calculations.
- Safety tables — controlled ledger, reconciliations, recall cases, adverse events, temperature readings.
- Patient ID privacy hardening — the encryption-at-rest columns for patient IDs.
Every migration is idempotent (guarded creates and index checks) and reversible, and runs against the primary database and every tenant database. No stock, payment, supplier or product master tables are created — those live in the modules Pharmacy reuses.
Demo seeder
Load realistic sample data into a fresh install with php erpat db:seed PharmacyDemo, and roll it back out (a soft delete of exactly what it inserted) with php erpat db:seed PharmacyDemo --remove. It is idempotent and reversible, and also available from the tenant Bulk Database Seeder modal.
Reuse boundaries & multi-tenancy
Per ERPat's no-duplication rules, Pharmacy never stands up a second stock ledger, payment ledger, supplier master or product catalog. It reads and writes through the modules that already own those concerns:
| Reuses | For | Pharmacy never clones |
|---|---|---|
| POS | The retail register and BIR receipt series the dispensing receipt reuses. | A separate sales register / receipt numbering. |
| Warehouse | The stock ledger and lot/batch/expiry (FEFO) engine; the FEFO stock issue and recall batch quarantine compose its public API. | A second stock ledger or expiry engine. |
| Procurement | Purchase orders, goods receipts, and the vendor master (extended with supplier FDA-LTO licensing). | A parallel supplier master. |
| Finance (tax) | The VAT / tax master for pharmacy line taxes. | Its own tax tables. |
| Clients | The customer master (retail patient records are kept separately for privacy). | A duplicate customer table. |
The catalog, near-expiry dashboard and FEFO dispensing all degrade gracefully — with a clear notice — when the Warehouse (WMS) or Procurement module is not installed.
Multi-tenancy. Everything Pharmacy stores lives in each company's own tenant database, so branches, licenses, credentials, patients, prescriptions and settings are fully isolated between tenants. Because migrate:modules runs across the primary DB and every tenant DB, each tenant that enables the module gets the same schema — enable and configure Pharmacy per tenant.
Next: Reference → — the controllers, routes, permission keys, tables and API endpoints.