Shared Package
@activation-sys/shared — Types, Zod schemas, constants, and error classes used across all apps.
Installation
Already included in the monorepo workspace.
import { createUserSchema, AppError, SIM_LIMITS } from '@activation-sys/shared';Exports
// TypeScript types
export * from './types/index';
// Zod validation schemas
export * from './schemas/index';
// Constants
export * from './constants/index';
// Error classes
export { AppError, AuthError, ValidationError, NotFoundError, ActivationError, PaymentError, OperatorError, SupportError, toApiResponse };
// Notification errors
export { NotificationError, notificationError, NOTIFICATION_ERROR_CODES, NOTIFICATION_ERROR_MESSAGES }
export type { NotificationErrorCode }Types
Status and role types are string unions (not TypeScript enums):
type UserRole = 'end_user' | 'b2b_admin' | 'activation_officer' | 'super_admin' | 'support' | 'admin';
type ManageableArea =
| 'activation-records' | 'packages' | 'officer' | 'support' | 'b2b' | 'notifications'
| 'customers' | 'store' | 'identity-detail' | 'audit'
| 'staff-users' | 'company-people' | 'company-onboarding';
type ActivationStatus = 'pending' | 'in_progress' | 'activated' | 'failed' | 'cancelled';
type OperatorSlug = 'stc' | 'mobily' | 'zain';
type IdentityType = 'citizen' | 'resident' | 'visitor';
type PaymentMethod = 'mada' | 'stc_pay' | 'apple_pay' | 'google_pay' | 'visa' | 'mastercard' | 'stripe';
type SupportedCurrency = 'USD' | 'SAR' | 'AED' | 'EUR' | 'GBP' | 'EGP';types/areas.ts also holds the area rules. VIEW_ONLY_AREAS (activation-records, customers, identity-detail, audit) are granted at View only, and the admin user schemas reject them in managedAreasWrite. AREA_REQUIRES maps a dependent area to its parent (identity-detail to customers, company-people and company-onboarding to b2b), and STAFF_ROLE_REQUIREMENTS lists what a grantor must hold for the fixed roles (activation_officer: officer Manage and customers View; support: support Manage and customers View). effectiveLevels(managedAreas, managedAreasWrite) turns an admin's stored pair into per-area View or Manage levels. canGrant(actor, target) says whether an actor may hand out a role and areas (only levels it holds itself, never staff-users Manage, anything for the owner), and coversUser(actor, target) says whether the actor is not weaker than an existing user, so may edit or remove them (never the owner). The ADMIN_USER_SCOPE_EXCEEDED error code (403) covers a change outside the actor's own access.
This shared union retains legacy values for existing consumers. Runtime API methods are mada, visa, mastercard, apple_pay and stc_pay only; gateway selection is Mock/Moyasar/Paymob (PAYMENT_GATEWAY_IDS = moyasar, paymob). See payment contract.
Support and Officer Types
| Type or constant | Contract |
|---|---|
Ticket | Full ticket row, including type/source, links, due/response/decision timestamps and refund decision fields |
CustomerTicket, CustomerTicketDetail | Customer-safe ticket fields and non-internal comments; staff author ids are hidden |
StaffTicketListItem, StaffTicketDetail | Staff ticket fields, masked customer, assignee, overdue/awaiting-response markers, comments, audit history and linked-record state |
TicketCounts | open, inProgress, overdue, awaitingResponse, awaitingDecision |
OfficerQueueActivationItem | Fallout row with claim, age, company flag and live refund request id |
OfficerActivationEvent | One status change of an activation: id, fromStatus, toStatus, source, note, createdAt |
OfficerQueueActivationDetail | Queue or completion-tool row plus heldState, completion claim, package, serial suffix, payment, operator capabilities and events |
OfficerQueueStoreItem | Read-only store reconciliation row |
StoreOrderStatusEvent | One issuance change of a store order: id, fromState, toState, errorCode (the code after the change), source (StoreTransitionSource), note, createdAt |
OfficerQueueCounts | failed, stuck, claimedByMe, and store attention counts |
OfficerCompletionActivation | One paid, not-active activation as the completion tool lists it: package, operator, SIM, payment, stale, answerLost, claim and refund request state |
OfficerCompletionCustomer | A matched customer with their masked phone, identity type and up to 50 OfficerCompletionActivation rows |
OfficerCompletionSearchResponse | matchedOn (phone or serial) and the matched customers |
OfficerHeldState | retryable, needs_investigation, in_flight, none |
OfficerOperatorLookup | Which operator lookup answered: request (by our reference) or line (by SIM serial) |
OfficerOperatorCheckOutcome | What a direct operator check did: activated, retry_allowed, not_active, check_again |
OfficerOperatorCheckResponse | activationId, lookup, the operator's raw state, and the outcome |
OfficerResolveResponse | activationId and the resulting status, after resolve records a confirmed outcome |
OperatorCapabilities | repeatSafeActivation?, requestLookup?, lineLookup?, each absent or false meaning the operator does not offer it |
OperatorLookupState | active, not_found, in_progress — an operator lookup's raw answer |
TICKET_TYPES | customer_request, refund_request |
TICKET_SOURCES | customer, staff |
TICKET_STATUSES | open, in_progress, resolved, closed |
TICKET_MARKERS | overdue, awaiting_response |
REFUND_REASONS | service_not_delivered, activation_failed, duplicate_charge, customer_request, other |
REFUND_DECISIONS | approved, rejected |
TICKET_DUE_WINDOW_DAYS is 5, TICKET_REASON_MAX_LENGTH is 500, and TICKET_REFERENCE_PREFIX is TKT-.
REFUND_DECIDER_ROLE (constants/support.ts) names the one role that decides refund requests, currently super_admin; isRefundDecider(role) tests membership. The decision route admits only this role, and the same role is refused before any read on both refund-request-opening routes, so a request is never left without a possible decider. customerTicketPath(ticketId), supportTicketPath(ticketId), and officerTicketPath(ticketId) build the in-app link paths the ticket notifications carry; STAFF_TICKET_PATH_PREFIXES (/support/tickets/, /officer/tickets/) lists the staff paths, which carry no locale segment, for a portal to recognise and follow as its own pages. PAID_ORDER_STATUSES (paid, activating, completed) and isPaidOrderStatus(status) define the order statuses that count as paid for support work: a refund request can be opened only for one of them, and the customer record's orderPaid flag reads the same set.
Notification Types
types/notification.ts exports the notification vocabulary and contracts through the package root.
| Type | Shape or Values |
|---|---|
NotificationLabel | Readonly en and ar strings. |
NotificationLocale | en, ar. |
NotificationChannel | in_app, email, sms, push. |
NotificationChannelEntry | Readonly bilingual label and enabled flag. |
NotificationQueuedChannel | email, sms. |
OptOutEligibleChannel | email, sms, push. |
NotificationCategory | activation, payment, esim_delivery, company, admin, system, announcement. |
NotificationCategoryEntry | Readonly bilingual label and transactional flag. |
NotificationSeverity | info, warning, critical. Presentation only. |
NotificationUrgency | normal, urgent. Selects queue priority only. |
NotificationEventKey | activation.completed, activation.failed, payment.succeeded, company.approved, contract.ready, admin.invited, esim.issued, ticket.opened, ticket.assigned, ticket.replied, ticket.resolved, ticket.refund_requested. |
NotificationTemplateKey | The same event keys plus custom. |
NotificationDeliveryOutcome | queued, sending, sent, delivered, failed_terminal, suppressed. |
NotificationRetryBookkeeping | attempts, nullable errorCode, nullable errorText and nullable failedAt. Separate from the outcome. |
NotificationSuppressionReason | account_inactive, channel_opted_out, no_address, provider_unsupported. |
NotificationBatchStatus | scheduled, running, finished, cancelled. |
NotificationAudienceKind | users, company, role, all. |
NotificationAudience | users carries readonly userIds. company carries companyId. role carries a UserRole. all carries no selector. |
EventNotificationTriggerInput | kind: 'event', eventKey and the common trigger fields below. |
CustomNotificationTriggerInput | kind: 'custom', category and the common trigger fields below. |
NotificationTriggerInput | Union of the event and custom trigger inputs. |
NotificationTriggerResult | recipientsTotal, notificationsCreated, notificationsDeduped, deliveriesCreated, deliveriesQueued, deliveriesSuppressed and fullyEnqueued. |
NotificationEmailJob | deliveryId, notificationId and channel: 'email'. No address or content. |
NotificationSmsJob | deliveryId, notificationId and channel: 'sms'. No address or content. |
NotificationChannelJob | Union of the email and SMS jobs. |
NotificationOrchestrationJob | run_batch with batchId, reconcile_handoffs with staleAfterMs and batchSize, or normalise_callback with eventId. Each branch uses the kind discriminator. |
RenderedNotification | Required body. Optional subject, html and attachments. Each attachment has filename, Buffer content and cid. |
NotificationRenderFn | Async function receiving templateKey, params, channel, locale and optional { userId } context. Returns RenderedNotification. |
NotificationSendResult | accepted, rejected_retryable, rejected_terminal, unsupported. |
NotificationSendOutcome | Required result. Optional provider, providerMessageId, errorCode and errorText. |
NotificationProviderCallbackEvent | externalEventId, eventType, nullable providerMessageId, normalisedStatus, nullable reasonCode and nullable providerTimestamp. Status is sent, delivered, failed_terminal or ignored. |
NotificationSendMeta | deliveryId, notificationId, queued channel and locale. |
NotificationSendFn | Async function receiving the loaded address, rendered content and NotificationSendMeta. Returns NotificationSendOutcome. |
Both trigger branches require audience, params and dedupeKey. Optional fields are severity, urgency, channels, expiresAt, batchId and createdBy. Event params carry validated identifiers and template inputs. Custom params carry bilingual copy. Addresses and credentials are not trigger params. A supplied channel list narrows the registered channels. The dedupe key identifies one occurrence per recipient.
Notification jobs do not extend BaseJob. They have no payload type, jobId or createdAt fields. JobType and QueueJob in types/queue.ts cover activation, payment and store only. Use the notification job types directly for the three notification lanes.
A chunk transaction that does not commit makes the trigger throw after logging ids only, so a caller never treats a partial write as a complete occurrence. The dedupe key retries the same occurrence later. fullyEnqueued can be false after storage succeeds. The flag does not prove delivery. Disabled push creates no delivery row. In-app delivery is written directly as delivered. Only announcement is non-transactional and consults channel opt-outs.
Allowed outcome changes are explicit. Repeated or backward changes are rejected by canAdvanceDeliveryOutcome.
| Current Outcome | Allowed Next Outcomes |
|---|---|
queued | sending, sent, delivered, failed_terminal, suppressed |
sending | sent, delivered, failed_terminal, suppressed |
sent | delivered, failed_terminal |
delivered | None |
failed_terminal | None |
suppressed | None |
Zod Schemas
All schemas use Zod v4 and can be used for both validation and type inference:
import { createUserSchema, activationSchema, paymentIntentSchema } from '@activation-sys/shared';
// Validate request body
const result = createUserSchema.safeParse(body);
if (!result.success) { /* handle validation error */ }
// result.data is typed as CreateUserAvailable schemas: createUserSchema, updateUserSchema, operatorSchema, createOperatorSchema, packageSchema, createPackageSchema, packageFilterSchema, activationSchema, iccidValidationSchema, activationResponseSchema, paymentSchema, paymentIntentSchema, paymentStatusSchema, paymentMethodSchema, ticketSchema, createTicketSchema, createAdminUserSchema, updateAdminUserSchema.
Support schemas include customerCreateTicketSchema, staffCreateTicketSchema, ticketCommentSchema, staffTicketCommentSchema, assignTicketSchema, ticketStatusChangeSchema, officerTicketStatusSchema, createRefundRequestSchema, decideRefundSchema, customerTicketListQuerySchema, and staffTicketListQuerySchema. They enforce strict bodies, one-or-zero links for customer tickets, exactly one link for refunds, 5,000-character ticket text, 500-character reason text, UUID ids, and keyset page limits.
Officer schemas (schemas/officer.ts): officerResolveSchema (outcome, evidence, reason), officerQueueQuerySchema (cursor, limit, kind, and at most one of phone, a serial-shaped serial restricted to the activations tab, or a UUID orderId restricted to the store tab), officerCompletionSearchQuerySchema (exactly one of phone or serial; a phone with a leading trunk 0 is read as Saudi and normalised to E.164, a serial is 19 or 20 digits), and officerCreateTicketSchema (activationId, subject, description, reusing the support ticket subject and body schemas).
Store schemas (from schemas/store.ts): supportedCurrencySchema, moneySchema, storeProductSchema, adminStoreProductSchema, storeDestinationSchema, storeDataAllowanceSchema, storeValiditySchema, storeProductListResponseSchema, storeDestinationsResponseSchema, storeProductListQuerySchema, storeDestinationsQuerySchema, createStoreOrderRequestSchema, storeOrderResponseSchema, storeOrderViewSchema, adminStoreOrderViewSchema, storeOrderListQuerySchema, adminStoreOrderListQuerySchema, storeOrderListResponseSchema, adminStoreOrderListResponseSchema, adminStoreProductListQuerySchema, adminStoreProductListResponseSchema, updateStoreProductSchema, updateStoreProviderSchema, esimIssuanceStateSchema, providerCatalogItemSchema, storeProviderHealthSnapshotSchema, fxRateEntrySchema, fxRatesResponseSchema, storeProviderSchema, storeProvidersResponseSchema, deviceCompatibilityEntrySchema, deviceCompatibilityResponseSchema.
Notification Request and Response Schemas
Every schema below is exported from schemas/notification.ts through the package root.
| Schema | Contract |
|---|---|
notificationChannelSchema | in_app, email, sms, push. |
notificationQueuedChannelSchema | email, sms. |
optOutEligibleChannelSchema | email, sms, push. |
notificationCategorySchema | activation, payment, esim_delivery, company, admin, system, announcement. |
notificationSeveritySchema | info, warning, critical. |
notificationUrgencySchema | normal, urgent. |
notificationTemplateKeySchema | The twelve registered event keys plus custom. |
notificationEventKeySchema | The twelve registered event keys. |
notificationDeliveryOutcomeSchema | queued, sending, sent, delivered, failed_terminal, suppressed. |
notificationSuppressionReasonSchema | account_inactive, channel_opted_out, no_address, provider_unsupported. |
notificationBatchStatusSchema | scheduled, running, finished, cancelled. |
notificationProviderSlugSchema | 1 to 64 characters. Starts with a lowercase letter or digit. Remaining characters may also include _ and -. |
inboxFeedQuerySchema | Optional cursor, limit from 1 to 50 with default 20, unreadOnly with default false, and archived with default false. Accepts query strings true and false for both booleans. |
inboxNotificationSchema | UUID id, eventKey, category, template key, nullable severity, rendered title and body, nullable action and image URLs, degraded flag for a failed render, nullable batch ID, creation time, and nullable read, seen, expiry and archive times. Timestamps must be RFC 3339. |
inboxFeedResponseSchema | data, nullable nextCursor and hasMore. No total. |
unreadNotificationCountResponseSchema | Nonnegative integer count. |
notificationIdParamSchema | UUID id. |
markNotificationReadResponseSchema | UUID id and readAt. |
markAllNotificationsReadResponseSchema | Nonnegative integer updated. |
markNotificationsSeenSchema | 1 to 200 UUIDs in notificationIds. Removes duplicates after the input bound is checked. |
markNotificationsSeenResponseSchema | Nonnegative integer updated. |
archiveNotificationResponseSchema | UUID id and archivedAt. |
notificationChannelStateSchema | Channel id, bilingual label, enabled and optOutAllowed. |
notificationOptOutsResponseSchema | Channel states in channels and the user's eligible channel list in optOuts. |
updateNotificationOptOutsSchema | Full replacement channels list. Removes duplicates. An empty list clears opt-outs. Rejects in_app. |
notificationAudienceSchema | Strict kind branches. users requires 1 to 1000 UUIDs. company requires a UUID companyId. role requires a UserRole. all takes no other fields. |
adminSendNotificationSchema | Strict union requiring exactly one of content or template. Requires category, audience and at least one channel. Urgency defaults to normal. Severity is optional. Schedule and expiry accept null or offset-bearing timestamps. A supplied schedule must precede a supplied expiry. |
adminSendNotificationResponseSchema | batchId, status, recipientsTotal, deliveriesQueued, deliveriesSuppressed, fullyEnqueued and nullable scheduledAt. Reports acceptance, not delivery. |
adminNotificationBatchListQuerySchema | Optional cursor, status and category. limit ranges from 1 to 100 with default 20. |
adminNotificationBatchSummarySchema | id, category, status, recipientsTotal, counts, nullable createdBy, nullable createdByName, createdAt and nullable scheduled, started, finished and cancelled times. |
adminNotificationBatchListResponseSchema | data, page-length total, nullable nextCursor and hasMore. |
notificationBatchCountsSchema | Nonnegative integer sent, delivered, failed, suppressed and pending counts. |
notificationDeliveryFailureSchema | Delivery and notification IDs, channel, outcome, nullable provider, attempts, nullable error code and text, nullable suppression reason and nullable failure time. |
adminNotificationBatchDetailResponseSchema | batch, aggregate counts and a separate failures page with data, total, nullable nextCursor and hasMore. |
cancelNotificationBatchResponseSchema | UUID id, status and nullable cancelledAt. |
deliveryRateQuerySchema | Offset-bearing from and to, with from before to. Optional channel and provider slug. The window applies to delivery creation time. |
deliveryRateBucketSchema | Channel, nullable provider, nonnegative total, sent, delivered, failed, suppressed and pending counts, and a rate from 0 to 1. |
deliveryRateFailureReasonSchema | Channel, nullable provider, reason and nonnegative integer count. |
deliveryRateResponseSchema | from, to, buckets and failureReasons. |
adminNotificationRecipientSearchQuerySchema | q trimmed, up to 100 characters, defaulting to empty. A company search needs at least 2 characters. A member or staff search may leave q empty, which lists the set. kind of company, member or staff, and optional UUID companyId, required only when kind is member. |
adminNotificationRecipientSearchResponseSchema | data, an array of { id, name } rows. No contact details or role. |
inboundNotificationWebhookParamsSchema | Queued channel and provider slug. |
Inbox and batch cursors preserve the full stored creation timestamp and UUID, separated by |. They order by creation time and ID descending. The custom admin branch validates its content directly. The keyed branch accepts { key, params }, where key is an event key rather than custom. Event-specific params are validated through the event registry before persistence.
| Inferred Type | Source Schema |
|---|---|
InboxFeedQueryInput | inboxFeedQuerySchema |
MarkNotificationsSeenInput | markNotificationsSeenSchema |
UpdateNotificationOptOutsInput | updateNotificationOptOutsSchema |
AdminSendNotificationInput | adminSendNotificationSchema |
AdminNotificationBatchListQueryInput | adminNotificationBatchListQuerySchema |
AdminNotificationRecipientSearchQueryInput | adminNotificationRecipientSearchQuerySchema |
DeliveryRateQueryInput | deliveryRateQuerySchema |
InboundNotificationWebhookParams | inboundNotificationWebhookParamsSchema |
CustomNotificationParams | customNotificationParamsSchema |
Notification Event Schemas
schemas/notification-events.ts exports these strict params schemas. UUID fields reject non-UUID values. Occurrence counters are nonnegative integers. Each event uses a template with the same key.
| Event Key | Params Schema | Required Params | Category | Registered Channels |
|---|---|---|---|---|
activation.completed | activationCompletedParamsSchema | UUID activationId, packageName from 1 to 255 characters | activation | in_app, email, sms, push |
payment.succeeded | paymentSucceededParamsSchema | UUID paymentId, UUID orderId | payment | in_app, email, push |
company.approved | companyApprovedParamsSchema | UUID companyId, UUID approvalAuditId | company | in_app, email, push |
contract.ready | contractReadyParamsSchema | UUID companyId, UUID contractId, readinessEpisode counter | company | in_app, email, push |
admin.invited | adminInvitedParamsSchema | UUID userId, inviteSequence counter | admin | in_app, email, sms |
esim.issued | esimIssuedParamsSchema | source: 'activation' with UUID activationId, or source: 'store' with UUID storeOrderId | esim_delivery | in_app, email, push |
ticket.opened, ticket.assigned, ticket.replied, ticket.resolved, ticket.refund_requested | ticketEventParamsSchema | UUID ticketId, reference matching TKT- plus 12 lowercase hex characters | system | in_app |
NotificationEventEntry defines readonly category, template, channels and params schema fields. NOTIFICATION_EVENT_REGISTRY contains all twelve entries. getNotificationEvent(key) returns an entry. A registered push channel remains disabled by the channel registry.
| Schema | Contract |
|---|---|
customNotificationParamsSchema | Strict object with bilingual title and body. Each language is trimmed and must remain nonempty. Titles allow 160 UTF-8 bytes per language. Bodies allow 1000 bytes per language. actionUrl and imageUrl are optional or null. |
notificationUrlSchema | 1 to 2048 characters. Accepts a root-relative path that does not start with //, or a parsed URL with the https: protocol. |
Constants
SIM_LIMITS // { citizen: 10, resident: 2, visitor: 1 }
QUEUE_NAMES // activation, payment, store-fulfillment, notification-email, notification-sms, notification-orchestration
RETRY_CONFIG // Per-queue retry configuration
ERROR_CODES // All error code constants
STORE_SUPPORTED_CURRENCIES // ['USD', 'SAR', 'AED', 'EUR', 'GBP', 'EGP']
BASE_CURRENCY // 'USD'
ESIM_ISSUANCE_STATES // ['pending', 'in_flight', 'provider_order_created', 'completion_in_flight', 'issued', 'pending_verification', 'reconciliation_required', 'failed']
ESIM_PROVIDER_ERROR_CODES // ['OUT_OF_STOCK', 'TRANSIENT_UNAVAILABLE', 'TERMINAL_FAILURE', 'AMBIGUOUS_RESULT']
STORE_TRANSITION_SOURCES // ['system'] — the source recorded on store order status events (StoreTransitionSource)
ICCID_LENGTH // 19
DEVICE_COMPATIBILITY // Versioned list of eSIM-capable phone models
DEVICE_COMPATIBILITY_VERSION // Current version string of the device list
resolveDeviceCompatibility // Canonical resolver: exact beats prefix, unknown never blocksNotification Constants
These exports are available from the package root. Vocabulary arrays and registry helpers live in types/notification.ts. Content bounds live in constants/notification.ts.
| Export | Value or Purpose |
|---|---|
NOTIFICATION_CHANNELS | in_app, email, sms, push. |
NOTIFICATION_CHANNEL_REGISTRY | Bilingual labels and enabled flags. In-app, email and SMS are enabled. Push is disabled. |
NOTIFICATION_QUEUED_CHANNELS | email, sms. |
OPT_OUT_ELIGIBLE_CHANNELS | email, sms, push. |
NOTIFICATION_CATEGORIES | activation, payment, esim_delivery, company, admin, system, announcement. |
NOTIFICATION_CATEGORY_REGISTRY | Bilingual labels and transactional flags. Only announcement is non-transactional. |
NOTIFICATION_SEVERITIES | info, warning, critical. |
NOTIFICATION_URGENCIES | normal, urgent. |
NOTIFICATION_EVENT_KEYS | The twelve registered activation, payment, company, admin, eSIM and ticket event keys. |
CUSTOM_TEMPLATE_KEY | custom. |
NOTIFICATION_TEMPLATE_KEYS | activation.completed, payment.succeeded, company.approved, contract.ready, admin.invited, esim.issued, custom. |
NOTIFICATION_DELIVERY_OUTCOMES | queued, sending, sent, delivered, failed_terminal, suppressed. |
NOTIFICATION_OUTCOME_PROGRESSION | queued, sending, sent, delivered, in forward order. |
TERMINAL_NOTIFICATION_OUTCOMES | delivered, failed_terminal, suppressed. sent is not terminal in the shared transition rules. |
NOTIFICATION_SUPPRESSION_REASONS | account_inactive, channel_opted_out, no_address, provider_unsupported. |
NOTIFICATION_BATCH_STATUSES | scheduled, running, finished, cancelled. |
NOTIFICATION_AUDIENCE_KINDS | users, company, role, all. |
NOTIFICATION_SEND_RESULTS | accepted, rejected_retryable, rejected_terminal, unsupported. |
isChannelEnabled(channel) | Reads the channel registry's enabled flag. |
isTransactionalCategory(category) | Reads the category registry's transactional flag. |
isNotificationEventKey(value) | Type guard for the six event keys. |
isTerminalDeliveryOutcome(outcome) | Tests membership in the terminal outcome array. |
canAdvanceDeliveryOutcome(from, to) | Checks the allowed transitions listed above. |
NOTIFICATION_EVENT_REGISTRY | Event-to-category, template, channels and params schema mapping. |
getNotificationEvent(key) | Returns the registered event entry. |
MAX_CUSTOM_TITLE_BYTES | 160 UTF-8 bytes per language. |
MAX_CUSTOM_BODY_BYTES | 1000 UTF-8 bytes per language. |
MAX_NOTIFICATION_URL_LENGTH | 2048 characters per URL. |
MAX_EXPLICIT_RECIPIENTS | 1000 user IDs per explicit audience. |
NOTIFICATION_RECIPIENT_CHUNK_SIZE | 500 recipients per processing chunk. |
MAX_SEEN_NOTIFICATION_IDS | 200 IDs per mark-seen request. |
RECIPIENT_SEARCH_LIMIT | 100 rows per recipient search. A member or staff search with no term is bounded to this cap in the database. |
NOTIFICATION_URL_SCHEMES | ['https:']. |
isAllowedNotificationUrl(value) | Allows a root-relative path except //, or a parsed URL using an allowed protocol. |
INBOX_MAX_LIMIT | 50. Exported from schemas/notification.ts. |
BATCH_LIST_MAX_LIMIT | 100. Exported from schemas/notification.ts. |
NOTIFICATION_ERROR_CODES | The ten notification codes listed under Error Classes. |
NOTIFICATION_ERROR_MESSAGES | English and Arabic messages keyed by NotificationErrorCode. |
Custom copy is stored per recipient. Byte limits apply separately to English and Arabic, not to their combined length.
| Queue Constant | Queue Name | Retry Constant | Attempts | Backoff |
|---|---|---|---|---|
QUEUE_NAMES.notificationEmail | notification-email | RETRY_CONFIG.notificationEmail | 5 | Exponential, 5000 ms base |
QUEUE_NAMES.notificationSms | notification-sms | RETRY_CONFIG.notificationSms | 3 | Exponential, 5000 ms base |
QUEUE_NAMES.notificationOrchestration | notification-orchestration | RETRY_CONFIG.notificationOrchestration | 5 | Exponential, 1000 ms base |
Error Classes
All errors extend AppError with bilingual messages:
import { ValidationError, NotFoundError } from '@activation-sys/shared';
throw new ValidationError('Validation failed', 'فشل التحقق', 'VALIDATION_ERROR', fieldErrors);
throw new NotFoundError('User not found', 'المستخدم غير موجود');
// Convert to API response (respects Accept-Language header)
toApiResponse(error, 'ar'); // Returns Arabic message
toApiResponse(error, 'en'); // Returns English message| Class | Status | Use Case |
|---|---|---|
AppError | Custom | Base error class |
AuthError | 401/403 | Authentication/authorization failures |
ValidationError | 400 | Request validation failures |
NotFoundError | 404 | Resource not found |
EsimProviderError | — | Classified eSIM provider failure thrown by adapters (code + optional externalOrderId) — internal to the fulfillment pipeline, never an HTTP envelope |
ActivationError | 422 | SIM activation failures |
PaymentError | 402/409 | Payment processing failures |
OperatorError | 502/504 | Operator API failures |
NotificationError | 400/401/404/409 | Notification failures with status selected by NotificationErrorCode |
SupportError | 403/409/422/503 | Ticket, refund, officer, and identity-start failures with status selected by SupportErrorCode |
IdentityError | 400/409 | Identity number and identity type failures with status selected by IdentityErrorCode |
Notification Errors
NotificationError(messageEn, messageAr, code, details?) accepts only a NotificationErrorCode. notificationError(code, details?) creates the same error using NOTIFICATION_ERROR_MESSAGES. Both work with toApiResponse. The status mapping is internal and is not exported.
| Code | Status | Meaning |
|---|---|---|
NOTIFICATION_NOT_FOUND | 404 | The notification does not exist or belongs to another user. |
NOTIFICATION_BATCH_NOT_FOUND | 404 | The batch does not exist. |
NOTIFICATION_BATCH_NOT_CANCELLABLE | 409 | The batch has started or finished and can no longer be cancelled. |
NOTIFICATION_INVALID_CUSTOM_CONTENT | 400 | Custom content is invalid, exceeds its limits or contains a disallowed URL. |
NOTIFICATION_UNKNOWN_EVENT | 400 | The event key has no registry entry. |
NOTIFICATION_CHANNEL_DISABLED | 400 | The requested channel is declared but unavailable. |
NOTIFICATION_OPT_OUT_NOT_ALLOWED | 400 | The channel cannot be muted. |
NOTIFICATION_WEBHOOK_SIGNATURE_INVALID | 401 | The callback signature is invalid. |
NOTIFICATION_WEBHOOK_PROVIDER_UNKNOWN | 404 | The callback provider is unknown or unavailable for the channel. |
NOTIFICATION_IDEMPOTENCY_CONFLICT | 409 | The idempotency key was reused with a different request. |
These ten codes also appear in ERROR_CODES. Route-specific operational failures can use AppError without joining this typed catalogue.
SupportError
SupportError(code, messageEn, messageAr, details?) uses a fixed status map. It covers ticket scope and state (TICKET_NOT_FOUND, TICKET_CLOSED, TICKET_LINK_INVALID, TICKET_ASSIGNEE_INVALID, TICKET_ACTION_NOT_ALLOWED), refund decisions (REFUND_ORDER_NOT_PAID, REFUND_NOT_DECIDABLE, REFUND_ALREADY_DECIDED, REFUND_SELF_DECISION, REFUND_DECIDER_CANNOT_OPEN — the refund decider role never opens a refund request, 403, REFUND_STORE_APPROVAL_DEFERRED), officer actions (OFFICER_CLAIM_TAKEN, OFFICER_CLAIM_NOT_HELD, OFFICER_ACTION_NOT_ALLOWED, OFFICER_JOB_IN_FLIGHT, OFFICER_COMPANY_READ_ONLY, OFFICER_LOOKUP_FAILED — a direct operator lookup call failed or timed out, 502), and the safe identity-start failure IDENTITY_START_UNAVAILABLE.
IdentityError
IdentityError(code, details?) uses a fixed status map and fixed bilingual copy per code, shared by the customer profile and the customer identity check. ID_NUMBER_INVALID and ID_NUMBER_TYPE_MISMATCH (400) refuse an identity number that does not fit the identity type it is checked against, or fits the other type instead. IDENTITY_TYPE_LOCKED (409) refuses an identity type change while an identity check for the customer is waiting or once one has completed. IDENTITY_TYPE_COMPANY_MANAGED (409) refuses a change for a company employee, whose type comes from the company's roster.