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_atandread_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
pendingtodeliveredtoacknowledgedorrefused, 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:
- An intranet service calls
notify_partyon 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). - Another module can publish
IntranetNotifyon 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). NotificationService.createinserts the notification and one dispatch per requested channel (src/cluster_erp/modules/intranet/notifications/services.py:37-99).- For the e-mail channel,
fan_out_emailasks thepartiesmodule for the person's addresses throughResolveEntityEmails, takes the primary one, and asks the mail module to send throughSendTransactionalEmail(src/cluster_erp/modules/intranet/notify.py:20-65). - The e-mail dispatch is marked
sentwhen the system mailbox accepted the message, otherwiseskippedwith the reason, for example no address or no handler.sentmeans 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.pydocstring 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, andcontractare 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
IntranetNotifyoutsidesrc/cluster_erp/core/contracts.pywas not found insrc/; inside the module, notifications are raised throughnotify_party. - Push and SMS dispatches are recorded as
skippedwith the errorchannel not configured(src/cluster_erp/modules/intranet/notifications/services.py:90-96). - The dispatch row has
next_retry_atandattempt_no, but a retry job was not found insrc/ordeploy/(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 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.