Boostorder Knowledge Base
Knowledge Base › Product Director › Notification Feature — How It Behaves & Where the Edge Cases Are
Product Director

Notification Feature — How It Behaves & Where the Edge Cases Are

Notification Feature — How It Behaves & Where the Edge Cases Are

Summary: Behaviour notes, API-level payload spec, and edge cases for product decision-making.

Try it yourself: open the "Do" tab above for a side-by-side walkthrough of a recurring notification and a one-off notification, showing why their underlying dispatch mechanics are built completely differently.

Main content

How it's built

A notification is a single record — a "notification template" — that covers both the setup (who it goes to, on what trigger) and, for one-off and recurring types, the actual message content. There are 17 notification types (document, category, product, consumer, customer, e-invoice, payment reminder, ageing, statement-live, statement-monthly, message, promotion, promotion reminder, plus three defined but currently switched off: coupon, points, phone-number). Every type shares one record shape and one create/update payload — the type field determines which of the other fields actually matter, similar in spirit to how a coupon's type field determines what "who it applies to" means.

Each type is also restricted to a specific set of channels — for example, document notifications can go over app, email, WhatsApp, or LINE, while consumer/customer/e-invoice notifications are email-only, and "Message" is WhatsApp/LINE-only. This channel restriction is enforced server-side for the types that use grouped recipients (promotion/promotion reminder) and otherwise governed by configuration the frontend reads to build the form.

Three different triggering models, not one

  • Event-driven (document, e-invoice, consumer, customer, promotion): fires the instant the underlying event happens. No schedule exists for these.
  • Scheduled/recurring (payment reminder, ageing, statement-live, statement-monthly, promotion reminder): runs on a repeating cadence (daily/weekly/monthly at a set time) and, each run, re-evaluates which accounts/documents currently match its conditions.
  • One-off (product, category, message): not triggered by any event and not recurring — someone explicitly sends it immediately, or schedules it for one specific future date/time. A background dispatcher intentionally skips the "message" type entirely; the only way a message-type notification goes out is an explicit send action.

Architecture & API-level reference

The create/update payload

This is the real payload shape accepted by the create and update endpoints (both take the same shape). Fields are grouped by purpose; which ones are actually required depends on the notification's type and channel (noted per field).

Identity & routing

Field Type Required Constraint
name string No Internal display name.
type string Yes One of the 17 type values above. Effectively selects which other fields are read.
channel string Yes app, email, whatsapp, or line. For promotion/promotion_reminder, validated against that type's allowed-channel list server-side; other types' channel options are constrained by config, not a hard server-side enum check in the create request itself — worth tightening if a type's channel gets exposed to a less-trusted caller.
status string No (system-managed for WhatsApp) draft, pending, or active. For WhatsApp templates, the server forces this to pending on save/edit until Meta approval flips it to active — see below.

Triggers (triggers, shape depends on type)

Type Trigger payload
document document_type, document_statuses (list), document_credit_approval_statuses, document_price_approval_statuses, document_purchase_limit_approval_statuses, notify_on_document_create (bool), notify_on_document_update (bool).
promotion promotion_event — required, one of 5 enum values (approval_required, approver_approved, approver_rejected, status_changed, user_note_added). If the event is status_changed, new_status is also required (one of the promotion status enum values).
promotion_reminder reminder_type — required, exactly one value (pending_approval is the only one defined today).
All other types triggers accepted as a free-form nullable object — no shape enforced by the server for these.

Recipient targeting

Field Type Required Constraint
recipient.company_id, .tier_id, .chain_id, .channel_id, .classification_id, .region_id, .role_id arrays of IDs No Sent as one nested object with up to 7 independent ID-list filters, each optional. Not "excluded" toggles like coupon's eligibility flags — these are always "only these," an AND-of-populated-lists match.
send_to_primary_contact, send_to_other_contacts boolean No Contact scope — who among a matched company's contacts qualifies.
send_to_email, send_to_email2 boolean No Which of a qualifying contact's addresses to use (email channel).
send_to_einvoice_buyer_email boolean No Adds the e-invoice buyer's email as an extra recipient address, independent of the contact-based scope above.
send_to_rep, send_to_rep_manager, send_to_super_rep, send_to_fulfiller, send_to_referrer, send_to_transporter, send_to_customer boolean No Document-type-specific recipient roles in the order lifecycle — layered on top of, not instead of, the contact-scope rules above.
customer_condition string No Legacy exclusion toggle interacting with role filtering — see the known-limitations note below before building anything new on top of it.
recipient_groups array Required (present) for promotion/promotion_reminder; otherwise nullable Only valid values: approver, submitter, each distinct. This is a completely separate targeting model from the fields above — promotion notifications never use company/role/contact-scope targeting.
cc_list, send_to_custom_emails array of email strings Required-if-empty-together for promotion/promotion_reminder on the email channel (see content rule below) Straightforward CC/BCC address lists.

Content

Field Type Required Constraint
title string Required for promotion/promotion_reminder on the email channel Email subject line.
content string Required for promotion/promotion_reminder; free-form nullable otherwise HTML-sanitized server-side for the email channel — except for the document type, whose email content stays as submitted (it's escaped at render time instead). WhatsApp/LINE content is never sanitized here, because WhatsApp template text has to exact-match what Meta approved.
attachments array of media IDs No

Scheduling (only meaningful for scheduled/recurring and one-off types)

Field Type Required Constraint
trigger_type string Required for scheduled/recurring and one-off types send_now or schedule.
schedule_type string Required if trigger_type=schedule and type is scheduled/recurring daily, weekly, or monthly.
schedule_day_of_week — Required if schedule_type=weekly
schedule_day_of_month — Required if schedule_type=monthly
schedule_time — Required if trigger_type=schedule for scheduled/recurring or one-off types
scheduled_date date Required if trigger_type=schedule for one-off types (product, category, message) Combined server-side with schedule_time into a single scheduled_at timestamp — this field is computed, not stored as submitted.
conditions object Only read for 4 types (below)

Conditions — the only 4 types that have them

payment_reminder, ageing, statement_live, statement_monthly each accept exactly the same three condition fields: due_date_type, due_date_days, minimum_outstanding. No other type reads this object at all, even if submitted.

Recipient resolution is one shared function, not one per type

Document, e-invoice, payment reminder, ageing, statement, and message notifications all resolve "who actually gets this" through the same single resolution path — role filter (include-only) → contact scope (primary / other) → email-address scope (Email 1 / Email 2). This was a deliberate consolidation specifically so recipient behavior could be reasoned about in one place instead of six near-duplicate implementations. Any future notification type should route through this same resolver rather than reinventing recipient logic — that's both cheaper to build and avoids reintroducing bugs already fixed here once.

A known migration gotcha, worth protecting against in future work: role filtering used to be entangled with the customer_condition exclusion setting. That coupling was removed so role filtering now always behaves as "only contacts with this role," independent of customer_condition. Any notification template saved before that fix, with customer_condition set to "except selected" and specific roles chosen, silently changed meaning when the fix shipped — those roles now mean "only these roles," not "every role except these." No backfill/data migration shipped alongside the fix (there were reportedly no such templates in production at the time) — this is an argument for validating that assumption before touching this area again, and for writing a migration this time if any legacy templates are ever found in this shape.

WhatsApp template lifecycle

WhatsApp is not a plain send-and-forget channel for the affected types (payment_reminder, ageing, statement_live, statement_monthly, message, and document): every template's exact content must be pre-approved by Meta before it can be used.

  • Creating or editing a WhatsApp-channel template whose content changed sets status to pending automatically and kicks off a side-effect that registers/updates the template with the messaging provider.
  • Only once an external approval webhook flips the status to active can the template actually send.
  • The "Send Now" action explicitly checks this status and refuses to send (with a translated error) if the template isn't active yet — this is enforced at the service layer, not just hidden in the UI.
  • Because content sanitization is skipped for WhatsApp/LINE specifically (see Content, above), any future feature that touches WhatsApp content programmatically must preserve an exact match with the approved template text, or it will trigger a fresh (and blocking) re-approval cycle.

One-off vs. recurring dispatch — two different underlying mechanisms

  • Recurring/scheduled types are picked up by a background process that runs on its own interval, re-evaluating conditions and generating sends each time — there's no per-record "dispatch queue" pre-built ahead of time for these.
  • One-off types (message specifically, and by extension product/category) instead pre-create individual dispatch records ahead of the actual send when a schedule is set, so a background job can pick up exactly those rows at the right time. Saving a one-off template with no schedule set creates no dispatch rows at all — sending only happens when someone explicitly triggers it.
  • Editing a scheduled one-off template deletes its still-pending dispatch rows and rebuilds them from the current template state, specifically so a stale recipient list or stale content can never go out after an edit.
  • Changing a template's type after creation is handled as a full re-hydration into the new type's record shape (each type has its own underlying casts/validation) rather than a partial field update — this is a heavier operation than a normal field edit and is worth keeping in mind if type-switching is ever exposed more prominently in the UI.

Promotion notifications are a distinct sub-system

Promotion and promotion-reminder notifications don't use company/role/ contact-scope targeting at all — they target one of two fixed groups, "approver" or "submitter" (or both, depending on the event), resolved from the promotion's own approval-workflow state rather than from the recipient-resolution path used by every other type. There are 5 defined trigger events for promotion and exactly 1 for promotion_reminder (a recurring "still pending your approval" digest). Any future promotion event should be added to the fixed event enum server-side, not inferred from freeform trigger data, since new_status is only validated at all when the event is specifically status_changed.

Known limitations

  • Three notification types (coupon, points, phone_number) are fully modeled in the payload/config layer but marked inactive — don't assume they're usable without first flipping them on and validating the recipient/content paths, which haven't been exercised in production.
  • SMS is defined as a disabled channel option in configuration but has no active integration — treat it as not built, not just turned off.
  • The channel-per-type restriction (e.g. category/product notifications being app-only today) is enforced by configuration read by the frontend and, for promotion types, by server-side validation — but is not server-side-enforced as a hard rule for every type. A future integration that posts directly to the create/update endpoint without going through the admin UI could submit an unsupported channel for a given type today.
  • The legacy customer_condition / role-filter interaction described above is a real latent correctness risk for any pre-existing template that was never re-saved after the decoupling fix — worth a one-time data audit before this area sees further investment.
Was this helpful?