Skip to main content
Version: 1.2

9.1 App Notifications

Controller: AppNotificationController · Route: api/v1/AppNotification · GroupName: notifications · 6 endpoints · Manages the current user's in-app notification inbox — the InApp channel's durable record.

This controller is intentionally read/manage-only from the caller's own perspective — there is no Create/Update endpoint, because AppNotification rows are created exclusively as a side effect of the dispatch pipeline (Chapter 6), never directly by a client.


POST GetInbox

Purpose: paginated, filterable list of the current user's notifications (DataTable-style grid). Permission: Permissions.AppNotifications.View

Request:

{
"draw": 1,
"start": 0,
"length": 20,
"search": { "value": "", "regex": false },
"order": [ { "column": 0, "dir": "desc" } ],
"columns": [ { "data": "sentOn", "name": "", "searchable": false, "orderable": true } ]
}

Response (DtResult<AppNotificationDto>, unwrapped — no Result<T> envelope):

{
"draw": 1,
"recordsTotal": 34,
"recordsFiltered": 34,
"data": [
{
"id": "8b1e2f40-0000-0000-0000-000000000001",
"recipientUserId": "5f1a3b2c-0000-0000-0000-000000000001",
"title": "تم اعتماد فاتورة الشراء INV-2026-00451",
"body": "تم اعتماد الفاتورة بمبلغ 12500.00 ر.س بتاريخ 2026-07-04",
"channel": 4,
"channelName": "InApp",
"isRead": false,
"readOn": null,
"sentOn": "2026-07-04T09:12:03Z",
"eventKey": "PurchaseInvoicePosted",
"refType": "PurchaseInvoice",
"refId": "1a2b3c4d-0000-0000-0000-000000000099",
"isActive": true,
"displayOrder": 0,
"createdOn": "2026-07-04T09:12:03Z",
"lastModifiedOn": null,
"createdBy": "00000000-0000-0000-0000-000000000000",
"lastModifiedBy": null,
"is_Deleted": false,
"deletedOn": null,
"deletedBy": null
}
]
}

Business notes: the query is scoped server-side to RecipientUserId == currentUser.GetUserId() — a caller can never retrieve another user's inbox through this endpoint, regardless of View/ViewAll permission (note PermissionConstants.AppNotifications.ViewAll is defined but not referenced by any action in this controller — likely reserved for a future admin-facing endpoint).

Angular: poll or refresh on SignalR "messages" event receipt (see Chapter 10) rather than on a fixed timer, so the inbox grid updates immediately when a real-time push arrives. Flutter: same pattern — refresh the inbox list when the mobile SignalR/Firebase listener fires, plus a pull-to-refresh gesture. Backend: AppNotificationService.GetInboxAsync — LINQ projection filtered by current user, standard DataTable pipeline (_repository.DataTableFilter). Related APIs: GetUnreadCount, MarkAsRead.


GET GetUnreadCount

Purpose: badge counter — total unread notifications for the current user. Permission: Permissions.AppNotifications.View

Request: no body, no parameters.

Response:

{ "succeeded": true, "message": null, "data": 7 }

Angular: call on app bootstrap and after every MarkAsRead/MarkAllAsRead call to keep a header badge in sync; also refresh on SignalR "messages" receipt. Flutter: same — typically bound to a bottom-nav badge. Backend: COUNT(*) WHERE RecipientUserId = currentUser AND IsRead = false AND Is_Deleted != true. Related APIs: GetInbox, MarkAllAsRead.


GET GetDetails/{id}

Purpose: full detail view of a single notification (e.g. when the user taps a row in the inbox). Permission: Permissions.AppNotifications.View

Response (AppNotificationDetailsDto):

{
"succeeded": true,
"message": null,
"data": {
"id": "8b1e2f40-0000-0000-0000-000000000001",
"recipientUserId": "5f1a3b2c-0000-0000-0000-000000000001",
"title": "تم اعتماد فاتورة الشراء INV-2026-00451",
"body": "تم اعتماد الفاتورة بمبلغ 12500.00 ر.س بتاريخ 2026-07-04",
"channel": 4,
"channelName": "InApp",
"isRead": false,
"readOn": null,
"sentOn": "2026-07-04T09:12:03Z",
"eventKey": "PurchaseInvoicePosted",
"refType": "PurchaseInvoice",
"refId": "1a2b3c4d-0000-0000-0000-000000000099",
"isActive": true,
"displayOrder": 0,
"createdBy": "System",
"createdOn": "2026-07-04T09:12:03Z",
"lastModifiedBy": null,
"lastModifiedOn": null,
"imageUrl": null
}
}

Possible errors: succeeded:false with an entity-not-found message if id doesn't exist or belongs to another tenant (blocked by the ambient tenant query filter). Related APIs: GetInbox.


PUT MarkAsRead/{id}

Purpose: mark a single notification as read (sets IsRead = true, ReadOn = now). Permission: Permissions.AppNotifications.MarkRead

Request: id in the route, no body.

Response:

{ "succeeded": true, "message": "AppNotification is updated successfully", "data": "8b1e2f40-0000-0000-0000-000000000001" }

Angular: call when the user opens/expands a notification row; optimistically decrement the unread badge locally, then reconcile with a GetUnreadCount call. Flutter: call on tap; same optimistic-update pattern. Related APIs: MarkAllAsRead.


PUT MarkAllAsRead

Purpose: batch mark every unread notification for the current user as read. Permission: Permissions.AppNotifications.MarkRead

Request: no body.

Response (returns the count of rows updated):

{ "succeeded": true, "message": "7 notifications marked as read", "data": 7 }

Angular: call from an inbox "Mark all as read" action; refresh GetInbox and zero the badge immediately after success. Backend: AppNotificationService.MarkAllAsReadAsync performs a single batched update scoped to the current user, not a per-row loop.


DELETE Delete

Purpose: soft-delete (or restore) one or more of the current user's notifications from their inbox. Permission: Permissions.AppNotifications.Delete

Request (query ?restore=false|true, body is a DeleteDto[]):

DELETE /api/v1/AppNotification/Delete?restore=false
[ { "id": "8b1e2f40-0000-0000-0000-000000000001" } ]

Response:

{ "succeeded": true, "message": "AppNotification is deleted successfully", "data": "8b1e2f40-0000-0000-0000-000000000001" }

Business notes: this is a soft delete (Is_Deleted = true) purely from the recipient's inbox view — it does not affect the underlying NotificationDeliveryAttempt/analytics history, which remain intact for operator-facing reporting.