Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Business modules

Explanation

Intranet notifications and delivery

How the intranet inbox stores notifications, fans them out to channels including e-mail, and how official document delivery records a receipt.

This page explains three parts of the intranet module that carry messages to employees. The notifications sub-module in src/cluster_erp/modules/intranet/notifications/ is the in-app inbox. src/cluster_erp/modules/intranet/notify.py is the helper that raises a notification and hands the e-mail channel to the mail module. The delivery sub-module in src/cluster_erp/modules/intranet/delivery/ hands official documents to employees and records the receipt.

In short

  • A notification is stored once and fanned out as one dispatch row per channel: in-app is delivered at once, e-mail goes through the system mailbox, push and SMS are recorded as skipped (src/cluster_erp/modules/intranet/notifications/services.py:1-9).
  • Read state lives on the server as seen_at and read_at (src/cluster_erp/modules/intranet/notifications/services.py:193-231).
  • Notifications carry an English fallback title plus an i18n key with parameters, so the client renders them in the reader's language (src/cluster_erp/modules/intranet/notifications/services.py:53-58).
  • An official delivery moves from pending to delivered to acknowledged or refused, and the acknowledgement records who, when, and from which client address (src/cluster_erp/modules/intranet/delivery/services.py:26-30, src/cluster_erp/modules/intranet/delivery/services.py:221-243).

How it works

Notification path

flowchart TD
 A["intranet action: leave decision, request outcome, delivery, event change"] --> N["notify_party"]
 B["IntranetNotify event from the bus"] --> H["Module._handle_notify"]
 H --> S["NotificationService.create"]
 N --> S
 S --> R["IntranetNotification row"]
 S --> D1["dispatch in_app: delivered"]
 S --> D2["dispatch email: queued"]
 S --> D3["dispatch push or sms: skipped"]
 D2 --> F["fan_out_email"]
 F --> E1["ResolveEntityEmails"]
 E1 --> E2["SendTransactionalEmail"]
 E2 --> M["mark_email_dispatch: sent or skipped"]

In prose:

  1. An intranet service calls notify_party on its own session, so the notification commits with the action that caused it. The helper opens the row-level backstop for exactly the recipient for this insert (src/cluster_erp/modules/intranet/notify.py:1-6, src/cluster_erp/modules/intranet/notify.py:68-113).
  2. Another module can publish IntranetNotify on the EventBus. The handler writes into the caller's tenant database and commits (src/cluster_erp/modules/intranet/module.py:305-345, src/cluster_erp/core/contracts.py:2194-2208).
  3. NotificationService.create inserts the notification and one dispatch per requested channel (src/cluster_erp/modules/intranet/notifications/services.py:37-99).
  4. For the e-mail channel, fan_out_email asks the parties module for the person's addresses through ResolveEntityEmails, takes the primary one, and asks the mail module to send through SendTransactionalEmail (src/cluster_erp/modules/intranet/notify.py:20-65).
  5. The e-mail dispatch is marked sent when the system mailbox accepted the message, otherwise skipped with the reason, for example no address or no handler. sent means an attempt, not a delivery receipt (src/cluster_erp/modules/intranet/notifications/services.py:111-135).

The dispatch states are queued, sent, delivered, bounced, failed, and skipped; the channels are in_app, email, push, and sms (src/cluster_erp/modules/intranet/models.py:82-90). How the mail module sends is on mail module.

Inbox routes

Method and path Purpose Source
GET /notifications/ List within the caller's scope src/cluster_erp/modules/intranet/notifications/routes.py:31-64
GET /notifications/mine The caller's own inbox src/cluster_erp/modules/intranet/notifications/routes.py:67-89
GET /notifications/unread-count The caller's unread count src/cluster_erp/modules/intranet/notifications/routes.py:92-110
POST /notifications/read-all Mark all as read src/cluster_erp/modules/intranet/notifications/routes.py:113-132
GET /notifications/{id}/dispatches The channel rows of one notification src/cluster_erp/modules/intranet/notifications/routes.py:153-164
GET /notifications/{id}, POST .../read, POST .../seen Read one, mark read, mark seen src/cluster_erp/modules/intranet/notifications/routes.py:167-199

Counting or marking another person's inbox needs intranet_notification.read_all (src/cluster_erp/modules/intranet/notifications/routes.py:99-130). The metadata declares one action, Mark as Read (src/cluster_erp/modules/intranet/notifications/metadata.py:44-48).

Official delivery

A delivery (IntranetOfficialDelivery) has a code from the sequence intranet_official_delivery, a recipient party, a title, a category, a confidentiality flag, a link to a document version or an attachment, a period, a channel, and a signature requirement (src/cluster_erp/modules/intranet/models.py:936-1015, src/cluster_erp/modules/intranet/delivery/services.py:130-154). The categories are payslip, contract, certificate, disciplinary, commendation, notice, id_card, and other (src/cluster_erp/modules/intranet/models.py:125-134).

stateDiagram-v2
 [*] --> pending
 pending --> delivered: deliver
 pending --> acknowledged: acknowledge
 delivered --> acknowledged: acknowledge
 pending --> refused: refuse
 delivered --> refused: refuse

In prose: a delivery starts pending. Deliver sets delivered_at and notifies the employee in-app and by e-mail with the key intranet.notif_delivery. Acknowledge Receipt records the time, the client address, and an optional signature attachment; only the addressee can acknowledge. Refuse records a refusal (src/cluster_erp/modules/intranet/delivery/services.py:176-244).

Action Label Condition Source
download_document Open Document none src/cluster_erp/modules/intranet/delivery/metadata.py:98-106
deliver Deliver status is pending src/cluster_erp/modules/intranet/delivery/metadata.py:107-115
acknowledge Acknowledge Receipt status is delivered src/cluster_erp/modules/intranet/delivery/metadata.py:116-124
refuse Refuse status is delivered src/cluster_erp/modules/intranet/delivery/metadata.py:125-133

GET /deliveries/{id}/document streams the file only after the row-level check of the delivery. It resolves a linked document version to its file through ResolveDocumentVersionFile on the EventBus (src/cluster_erp/modules/intranet/delivery/routes.py:120-164).

Why it is built this way

  • The notify.py docstring names in-app as the guaranteed channel and e-mail and push as best-effort channels that never claim delivery (src/cluster_erp/modules/intranet/notify.py:1-6).
  • The dispatch model docstring states that the interface must not treat an unacknowledged notification as undelivered (src/cluster_erp/modules/intranet/models.py:655-660).
  • The delivery docstring states that publishing centrally is not enough; the employee must be shown to have received the document (src/cluster_erp/modules/intranet/delivery/services.py:1-4).

Rules and boundaries

  • The categories payslip, disciplinary, and contract are confidential unless the creator says otherwise (src/cluster_erp/modules/intranet/delivery/services.py:37, src/cluster_erp/modules/intranet/delivery/services.py:130-135).
  • A confidential delivery is visible only to its recipient and to holders of intranet_official_delivery.read_all, not to the recipient's manager (src/cluster_erp/modules/intranet/delivery/services.py:40-64).
  • Creating a delivery needs intranet_official_delivery.create; deliver and refuse need .update; acknowledge needs .read (src/cluster_erp/modules/intranet/delivery/routes.py:84-239).
  • Deleting a delivery sets deleted_at (src/cluster_erp/modules/intranet/delivery/services.py:246-251).

Interaction with other parts

Contract Direction Purpose Source
IntranetNotify intranet subscribes another module drops a notification into an inbox src/cluster_erp/modules/intranet/module.py:199-203
ResolveEntityEmails intranet emits e-mail addresses of the recipient party src/cluster_erp/modules/intranet/notify.py:78-82
SendTransactionalEmail intranet emits send through the system mailbox src/cluster_erp/modules/intranet/notify.py:91-99
ResolveDocumentVersionFile intranet queries the file behind a delivered document version src/cluster_erp/modules/intranet/delivery/routes.py:143-150

The contract list is on the contracts reference. Platform-wide notification rules are on notification rules.

Limits and open points

  • A construction of IntranetNotify outside src/cluster_erp/core/contracts.py was not found in src/; inside the module, notifications are raised through notify_party.
  • Push and SMS dispatches are recorded as skipped with the error channel not configured (src/cluster_erp/modules/intranet/notifications/services.py:90-96).
  • The dispatch row has next_retry_at and attempt_no, but a retry job was not found in src/ or deploy/ (src/cluster_erp/modules/intranet/models.py:655-686).

Terms introduced

Term Code identifier Definition Turkish term Source
notification IntranetNotification One message in a person's in-app inbox. Bildirim src/cluster_erp/modules/intranet/models.py:614
dispatch IntranetNotificationDispatch The record of one channel's attempt to deliver a notification. Gönderim kaydı src/cluster_erp/modules/intranet/models.py:655

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13