Falaah Falaah AI

Licenses and Permits

Track licenses and professional permits with a responsible owner, a five-tier expiration alert cadence, and a four-step renewal wizard with OCR expiry.

The Licenses module is Muin’s register of business licenses, professional permits, regulatory registrations, and certifications. Each license has a responsible owner, alert cadence tiers, renewal history, a linked calendar event, and an OCR pipeline that extracts the expiry date from the uploaded certificate so nobody has to type it twice.

If a license lapses because nobody was watching the calendar — the whole organization can be operating illegally for days, weeks, or months. This module exists so that doesn’t happen.

The Licenses List

On /compliance/licenses, each row shows:

Column What it shows
Name Business-facing name (e.g. “California Seller’s Permit”)
License number Issuing agency’s reference
Status active, expiring_soon, expired, suspended, revoked, pending
Issuing authority State / city / professional board
Expiration date Next expiry
Days to expiry Live countdown, colored by tier
Responsible user Accountable owner

Filters let you narrow by status, authority, category, or the assigned owner. The default filter excludes expired licenses that have been superseded by a renewal — see the renewal-filter note in the FAQs.


Creating a License

Click New License. The form asks for:

Field Purpose
Name Friendly descriptor
License number Issuing agency’s number
Category business, professional, regulatory, environmental, operational, other
Issuing authority The regulator or agency
Issue date When first issued
Effective date When this version takes effect
Expiration date Next expiry
Alert days Days before expiration to fire alerts — defaults to [90, 60, 30, 14, 7]
Responsible user Who gets the alerts
Annual fee Monetary cost for reporting
License document The PDF / image scan of the certificate
Renewal lead days How many days before expiration the renewal workflow unlocks

The alert_days field is a JSONB list on the license model (ExpirationMixin.alert_days) — per-license override is allowed if a particular jurisdiction wants a different cadence. The default matches the standard “90-60-30-14-7 urgency ramp” that works across most regulatory bodies.


The Five Alert Tiers (R11-EXPIRATION-WORKER)

A daily worker at 08:00 UTC walks every active license. For every tier in alert_days that the license’s time-to-expiry hits today, the worker:

  1. Fires a notification to the responsible_user via NotificationDispatcher
  2. Creates a HumanTask(category=LICENSE_RENEWAL) pointing to the license detail page
  3. Stamps license_alert_sent[tier] with the send timestamp (a JSONB @> containment check) so re-runs of the worker on the same day are no-ops

Idempotency

The license_alert_sent JSONB map is the worker’s idempotency ledger. It keys by tier:

{
  "90": "2026-01-15T08:00:00Z",
  "60": "2026-02-14T08:00:00Z",
  "30": "2026-03-16T08:00:00Z"
}

If the worker retries (e.g. after a transient failure), it sees the tier already stamped and skips the dispatch. Editing the alert_days list afterwards doesn’t reset the map — so adding a new 45 tier to a license that’s already past the 45-day mark doesn’t trigger a missed-alert blast.

When the Map Is Reset

The map is reset to {} on renewal — the license’s next cycle starts fresh, so the new 90-day mark will fire even if the previous cycle’s 90-day mark was already stamped.

Priority Mapping

Tier (days to expiry) HumanTask priority
7 CRITICAL
14 HIGH
30 MEDIUM
60 MEDIUM
90 LOW

Tuned so the 7-day-out alert pages the responsible user through whatever high-priority routing the tenant has configured (escalation emails, Slack @channel, etc.).


The Renewal Wizard (IS19)

Click Renew on a license within its renewal window. Muin opens a four-step wizard:

Step 1 — Pre-Fill

The wizard opens with the current license fields pre-populated. Review and adjust:

  • License number (if the agency assigns a new one on renewal)
  • Effective date (first day of the new period)
  • Expiration date (placeholder — gets overwritten after Step 3)
  • Annual fee amount
  • Notes

Step 2 — Receipt Upload

Upload the fee receipt from the issuing agency. This becomes part of the renewal history record. The upload goes through the standard Documents module pipeline (virus scan, versioning, retention tagging).

Step 3 — Certificate Upload + OCR

Upload the new license certificate (the renewed PDF / image from the agency). Muin’s Document Intelligence pipeline routes it to the license_cert extractor which pulls:

  • License number (verified against the pre-filled value; warns on mismatch)
  • Effective date
  • Expiration date — populates the form’s expiration field

The OCR suggestion is editable — if the extractor misreads a date (handwritten certificates, low-quality scans), correct it before submit.

Step 4 — Submit

Review the final values and click Confirm Renewal. The service performs an atomic update:

  • Records the renewal in renewal_history with previous + new expiration + fee + notes
  • Updates effective and expiration dates
  • Resets license_alert_sent to {}
  • Clears the old calendar event reference so the next worker pass creates a fresh one
  • Bumps version (optimistic lock counter)
  • Emits a compliance.license.renewed log event

Optimistic Locking (G-Licenses-5)

The atomic update is gated on version = :expected_version and bumps the counter in the same statement. If two owners click Renew simultaneously, only the first commit wins. The loser either:

  • Re-reads and returns idempotent success if its payload matches what’s now committed (same effective_date, expiration_date, fee)
  • Raises 409 LicenseRenewalConflictError if the payloads diverge

This keeps renewals race-safe without needing global locks.

Renewal Window Gate (G-Licenses-2)

You can’t renew a license that isn’t within its renewal window. The service raises:

License 'California Seller's Permit' is not within the renewal window of
90 days (expires 2026-07-15). Renewal is only allowed from 90 days before
expiration.

This prevents accidentally renewing a license 6 months early because someone clicked the wrong row.

Expired licenses can still be renewed — the window rejection only blocks premature renewals.


SOC 2 Evidence Auto-Update (JG-9)

When a license is renewed, the compliance.license.renewed event is consumed by the SOC Audit subscriber. If the license is linked to a framework control (typically via the compliance module mapping), the subscriber:

  1. Updates the evidence freshness date for the linked control
  2. Attaches the new license document as current evidence
  3. Marks the previous license document as archived evidence (retained for audit)

This means the auditor’s evidence-collection view stays current automatically — no manual “I renewed the license, now I need to remember to update the SOC 2 control” step.

Linking is configured on the license detail page under Linked Controls.


Calendar Integration

Each active license has a linked calendar event (calendar_event_id) representing the expiration date. The expiration worker regenerates this event on renewal so the calendar always shows the current cycle. Events appear on:

  • The responsible user’s personal calendar (via the tenant’s Calendar integration)
  • The tenant-wide Compliance Calendar at /compliance/calendar (if enabled)

Deleting a license removes its calendar event; renewing it moves the event to the new expiration date.


FAQs

How do reminders work?

A daily worker fires at 08:00 UTC. For each license, it compares today to expiration_date - tier for every tier in alert_days (default [90, 60, 30, 14, 7]). If today matches any tier and that tier hasn’t already been stamped in license_alert_sent, the worker dispatches a notification + HumanTask to the responsible user. Priority ramps from LOW (90-day) through CRITICAL (7-day). Re-runs of the worker on the same day are idempotent via the JSONB containment check.

What if the OCR gets my expiry date wrong?

The extracted value is a suggestion — it populates the form, but the field is editable. Review before submit. If you renew a license with a wrong expiry date, the renewal is atomic (either fully applied or not at all) and the renewal_history retains the submitted value, so correcting it is a matter of entering a new renewal with the right date. Common OCR pitfalls: handwritten dates on old certificates, low-contrast scans, non-US date formats — adjust manually when those bite.

How do I renew manually?

If the renewal wizard doesn’t fit your flow (e.g. you renewed through the agency’s portal months ago and you’re just catching up Muin), you can update the license directly: edit the license, change effective + expiration dates, save. The license_alert_sent map clears on that update so the next cycle’s alerts fire clean. The renewal wizard is preferred because it captures receipt + OCR-verified expiry + SOC 2 auto-update — but manual updates are a supported fallback.

Why can’t I renew a license that expires in six months?

The renewal window guard (G-Licenses-2) rejects renewals outside renewal_lead_days before the expiration. The default is 90 days; you can raise it per-license on the edit form if your jurisdiction allows earlier filing. The rejection error message includes the exact window so you know when the renewal unlocks.

What happens to the alert map when I change alert_days?

Nothing — the map keys by tier value. Changing the tier list doesn’t reset the map. If you add a tier (say, 45), the worker will fire for 45 the next time a license’s time-to-expiry hits that value. If you remove a tier (say, removing 90), stamps for the removed tier are harmless leftovers — they don’t fire anything. Resets only happen on renewal.