AFTEC Email Notifications (PROCESS.NOTIFY)

The legacy, AFTEC-native email system the .NET desktop suite uses to send email. Not to be confused with PSI Notify Bot (Teams/SMS/voice) — this is the older UniData-subroutine-backed path, invoked directly from desktop apps via WCF.


Overview

MetricValue
BackendPROCESS.NOTIFY — UniData BASIC subroutine on the AFTEC server
TransportWCF → PSI Local Service → RunSub("PROCESS.NOTIFY", ...)
Client wrapperPSI.DataAccess.Services.PSILocalService.ProcessNotify(...)
SourcePSI.Shared/trunk/PSI.DataAccess/Services/POMaintServices.cs (client-side wrapper only)
Subscription tableNOTIFY.SUB.1287 (AFTEC file)
Backend sourcePROCESS.NOTIFY / GETNEXTNOTIFYID — UniData BASIC, S:\LinuxShare\pro3prog\VBBASE\ (Unix path /home/ptis5/pro3prog/VBBASE/). Not in this git repo, not yet cataloged in the AFTEC subroutine catalog. Confirmed: UniData does not send the email. PROCESS.NOTIFY only resolves recipients and writes a manifest file to a watched share (see “How the send actually happens” below) — something downstream (almost certainly a Power Automate flow via an on-premises data gateway, per the SENDTOGATEWAY label in the source) parses that file and sends via SMTP/Graph. The flow itself hasn’t been located yet.
Used byPSI.OpenPurchaseOrderInq.View, PSI.POMaintenance, and other desktop apps that need to email a person or a PO’s subscribers

This is distinct from the raw-SMTP path in PSI.Common.Helpers.EmailHelper (SmtpClient direct to smtp.progressivesurface.com) — that’s a separate, simpler mechanism some apps use for their own outbound mail (e.g. expedite templates) without going through AFTEC at all. PROCESS.NOTIFY is the one to reach for when the email should be tied to an AFTEC entity (a PO, a project) or when the recipient list should be dynamic/subscriber-driven rather than hardcoded.


Two ways to send

1. Direct — explicit recipient list

Pass real email addresses; leave eventID/eventType blank. The subroutine just sends to who you tell it to.

var notifyID = PSI.DataAccess.Services.PSILocalService.GetNextNotifyID();
PSI.DataAccess.Services.PSILocalService.ProcessNotify(
    new List<string> { vendorEmail }, subject, body,
    string.Empty, string.Empty, "purchasing@progressivesurface.com", notifyID);

Real example: PSI.OpenPurchaseOrderInq.View’s SendSelectedPOEmails / SendExpediteEmail (PurchaseOrderInquiryViewModel.cs) use this for vendor “order status request” and “expedite” emails.

2. Subscription-based — resolved server-side

Pass an empty recipient list, but populate eventID ("{company}!{entityType}!{entityID}", e.g. "1!10!0123456" for PO entity type 10) and eventType (a free-text label like "PO Creation" or "PO Receipt"). The subroutine looks up who’s subscribed to that entity in NOTIFY.SUB.1287 and mails them — the client never resolves an email address.

var notifyID = PSI.DataAccess.Services.PSILocalService.GetNextNotifyID();
PSI.DataAccess.Services.PSILocalService.ProcessNotify(
    new List<string>(), subject, body,
    $"1!10!{poNumber}", "PO Receipt", "receiving@progressivesurface.com", notifyID);

Subscriptions themselves are managed separately:

PSI.DataAccess.Services.PSILocalService.AddRemoveSubscriptionAsync(
    entityTypeID: "10", entityID: poNumber, userInitials: mechEngineerInitials, AddToSubscription: true);

Real examples:

  • PSI.POMaintenance’s SubscribeCSMEngineers() — when a Customer Supplied Material PO (Buyer "CS") is created, it looks up the project’s Mechanical/Controls/Sales engineer (PSILocalService.GetOneProject(projectNumber), in ContactServices.cs:1139) and subscribes each to entity type "10" for that PO number, then fires a "PO Creation" notify with an empty recipient list.
  • PSI.OpenPurchaseOrderInq.View’s NotifyProjectEngineerOfCsmReceiptInBackground() (added 2026-07-08, PR #199) — fires a "PO Receipt" event against the same PO number when CSM is physically received, landing on whoever was already subscribed at PO-creation time. No new subscription call needed since the engineers are already subscribed to that PO from step 1.

A 2022 code comment in GetNextNotifyID() (POMaintServices.cs) reads: “Subscriptions would most likely not be used from C# code. They are based on system ‘triggers’ such as PO Receipt, etc.” — meaning “PO Receipt” as a subscription trigger was anticipated years before anyone actually wired it up. The 2026-07-08 change is the first thing to do so.


Supporting pieces

GetNextNotifyID()

Calls GETNEXTNOTIFYID, which increments and returns a shared counter (D NOTIFY.1287,"IDX"). Optional — if ProcessNotify’s notifyID param is left blank, PROCESS.NOTIFY generates its own ID internally via the identical increment logic (its GETNEXTID: subroutine). Only call GetNextNotifyID() yourself when you need to know the ID before calling ProcessNotify — i.e. when there’s an attachment, since the attachment has to be dropped in a folder named for the ID first.

Attachments

If the email needs a file attached, copy it into a folder named for the notify ID before calling ProcessNotify:

\\ad.ptihome.com\DFS\DATA\UnixShare\dgwReportFiles\Temp\EmailNotify\Attachments\{notifyID}\
var attachmentFolder = $@"\\ad.ptihome.com\DFS\DATA\UnixShare\dgwReportFiles\Temp\EmailNotify\Attachments\{notifyID}";
if (!Directory.Exists(attachmentFolder)) Directory.CreateDirectory(attachmentFolder);
File.Copy(sourceFile, $"{attachmentFolder}\\{Path.GetFileName(sourceFile)}", true);

Note this is a different folder from the manifest file PROCESS.NOTIFY itself writes (see below) — Attachments\{notifyID}\ vs Notifications\{notifyID}. (Use the DFS path, not the legacy \\FS1\Data\... alias — see PSI DNS Standards.)

NOTIFY.SUB.1287

The subscription table itself. Modeled client-side by SubscriptionModel (PSI.Common.Models/Models/Notifications/SubscriptionModel.cs): an entity type + entity ID + a list of subscribed user IDs. Known entity type 10 = Purchase Order, 14 = Redbook, 20 = Part; other entity types likely exist but aren’t documented here yet.


How the send actually happens — confirmed from source

Read directly from S:\LinuxShare\pro3prog\VBBASE\PROCESS.NOTIFY (Unix path /home/ptis5/pro3prog/VBBASE/PROCESS.NOTIFY). UniData does not send the email. The subroutine’s entire job:

  1. Resolve the recipient list — either taken directly from INREC<3> (direct-recipient mode), or read from NOTIFY.SUB.1287 keyed by ENTID (subscription mode). For Redbook (entity type 14), it removes the current user from their own subscriber list first so people don’t get emailed about their own action.

  2. Build subject/body — for direct mode, whatever was passed in; for subscription mode, it’s synthesized from ENTITY.1287’s description + entity number + event name (with a part-type and PO-type special case).

  3. Resolve a notify ID — either the one passed in (INREC<7>), or self-generated via the same increment-a-counter logic as GETNEXTNOTIFYID.

  4. GOSUB SENDTOGATEWAY — formats every field with bracket-delimiter tags and writes a plain-text manifest file:

    /home/ptis5/dgwReportFiles/Temp/EmailNotify/Notifications/{notifyID}
    
    [To]user1@progressivesurface.com,user2@progressivesurface.com[-To-]
    [CC]...[-CC-]
    [BC]...[-BC-]
    [SendFrom]notifications@progressivesurface.com[-SendFrom-]
    [Subject]...[-Subject-]
    [Body]...[-Body-]
    [Importance]Normal[-Importance-]   (only written if importance was specified)
    

    3-character recipient tokens (e.g. bare initials like JPT) get @progressivesurface.com appended automatically before writing.

  5. Returns. That’s it — no SMTP call, no Graph call, nothing UniData-side actually transmits the email.

The SENDTOGATEWAY label is the tell: something external — almost certainly a Power Automate flow reading this share through an on-premises data gateway — watches .../EmailNotify/Notifications/ for new files, parses the bracket tags, and does the actual send (likely via an Outlook/O365 connector). That flow hasn’t been located yet; if you find it, update this page with its name/environment so the loop closes.

A dead code path worth noting: an earlier version wrote a full record to a NOTIFY.1287 file (see the commented-out NREC/WRITE NREC ON NOTIFY.1287 block) — a 2022-05-15 comment (TURNED OFF WRITE TO NOTIFY.1287) says this was disabled in favor of the gateway-file approach, so NOTIFY.1287 is legacy/unused by the current code path.


Key Source Files

FilePathPurpose
POMaintServices.csPSI.Shared/trunk/PSI.DataAccess/Services/GetNextNotifyID(), ProcessNotify(...), GetBuyerInfo(...)
NotificationServices.csPSI.Shared/trunk/PSI.DataAccess/Services/AddRemoveSubscriptionAsync(...), CreateNotifications(...)
SubscriptionModel.csPSI.Shared/trunk/PSI.Common.Models/Models/Notifications/Client model for NOTIFY.SUB.1287 rows
ContactServices.csPSI.Shared/trunk/PSI.DataAccess/Services/GetOneProject(projectID) — used to resolve a project’s Mech/Controls/Sales engineer before subscribing them
PurchaseOrderInquiryViewModel.csPSI.OpenPurchaseOrderInq.View/Trunk/PSI.OpenPurchaseOrderInquiry.View/ViewModels/Direct-recipient example (vendor expedite emails) + subscription-based example (CSM receipt notify)
POMaintenanceViewModel.csPSI.POMaintenance/Trunk/PSI.POMaintenance/ViewModels/SubscribeCSMEngineers() — subscription-write example
EmailHelper.csPSI.Shared/trunk/PSI.Common/Helpers/The separate, simpler raw-SMTP path (not PROCESS.NOTIFY)

  • Teams Notifications (PSI Notify Bot) — the newer, separate Teams/SMS/voice notification system (used by the ProApps release pipeline, IT alerts)
  • Broadcast System — the other cross-app messaging pattern in the desktop suite (real-time navigation sync, not email)
  • AFTEC Subroutine Reference — catalog of documented UniData subroutines (PROCESS.NOTIFY itself isn’t yet a cataloged entry)
  • PSI DNS Standards — why the attachment path uses \\ad.ptihome.com\DFS\... and not \\FS1\...

Last updated: July 8, 2026 Source: Analysis of PSI.DataAccess.Services.PSILocalService notify/subscription calls across PSI.All, done while implementing PR #199 (CSM receipt notification)