Skip to content

ShopFloor - Jobs & Integrations

Background processing, scheduled jobs, and external system integration. For wiring and DI setup see 10-architecture.md.


flowchart LR
subgraph Inbound[Inbound integrations]
FB[Fishbowl ERP<br/>MySQL]
ML[Mercado Libre]
HP[HP PCB API]
BR[BitRaser]
CA[Channel Advisor<br/>temp file]
PT[Partner systems<br/>via ExternalAPI]
WTS[Windows Test System]
end
subgraph App[CTLShopFloor]
HF[Hangfire jobs<br/>~18 recurring]
WAPP[WebAPP]
EXT[ExternalAPI]
end
subgraph Outbound[Outbound integrations]
BLOB[Azure Blob Storage]
MONGO[(MongoDB<br/>audit log)]
SEQ[Seq]
EMAIL[SMTP<br/>per-project]
FBOUT[Fishbowl inventory<br/>sync back]
end
FB --> HF
ML --> HF
BR --> HF
CA --> HF
HP --> WAPP
PT --> EXT
WTS --> WAPP
HF --> BLOB
HF --> EMAIL
HF --> FBOUT
WAPP --> MONGO
WAPP --> SEQ

AspectDetail
Backing storeSQL Server (same connection family as the main DB)
Dashboard/hangfire (authenticated) — only registered when JOBS:allowJobsExecution=true
RegistrationServiceExtensions.ConfigureBackgroundJobs in WebAPP
GatingAll recurring-job registrations skipped if JOBS:allowJobsExecution=false. This is the prod-safety mechanism — dev machines should never have it set to true.
Error notificationsBaseJob.EmailDevelopersOnError() (⚠️ wrapped in bare catch {} so failures may be silent)

~18 active recurring jobs + a few disabled / stub jobs. Cron times are UTC; CST offset noted where the comment in code calls it out.

JobPurposeSchedule (UTC)Touches
NotifyUsersOfProductEOLJobEmails owners when products reach EOL in 30/60/90 days. Pulls from ProductEOL.DailyEmail via NotificationRecipient (Notifications.ProductEOL)
SyncSalesAndInventoryWithWorkTrackingJobBidirectional inventory sync for the two hardcoded tenants.30 11,22,13-23/1 * * * (6:30 AM, 5:30 PM, hourly 8:30 AM–4:30 PM CST)Fishbowl ↔ WorkTracking. ⚠️ Hardcoded tenant IDs.
PurgeErrorLogJobDeletes aged rows from the Serilog ErrorLog table.Daily at 10:00None
TranslateSystemInformationEntriesJobProcesses WorkTracking rows missing SystemInformation; maps from EngineeringData.0 1-3,14-23/1 * * 1-5 (7 PM–9 PM + 8 AM–5 PM CST, weekdays)Internal
AssignSystemVarianceCodeToNewUnitsJobAssigns SystemInformationVarianceCode to units lacking one.Same as aboveInternal
GenerateAndNotifyOfHpCoaReportJobMonthly CSV of DPKs injected last month, emailed to recipients.30 14 1 * * (8:30 AM CST, 1st of month)HP COA (DPK) data, email. ⚠️ Hardcoded tenant ID 36650844-da39-4b72-bf8f-ec989db49a27.
WorkOrderAcquisitionCostJobMonthly: reports work orders where acquisition cost wasn’t fully assigned.30 13 1 * * (7:30 AM CST, 1st of month)Email via Notifications.WorkOrderAcquisitionCost
BitraserLogDownloadJobDownloads drive erase + verification logs from BitRaser API; persists to DriveEraseLog / DriveVerificationLog.Daily at 3:00 (3:00 AM CST)BitRaser API
GenerateTrackingReportWithSixMonthsDaily 6-month tracking Excel report; uploads to Azure Blob trackingreport{tenantname}; deletes blobs older than 14 days.Daily at 9:00 (1:00 AM CST)Azure Blob Storage
FishbowlSalesImportJobImports prior-day fulfilled/shipped sales orders from Fishbowl into ProductSalesOrder + line items.Daily at 7:30 (1:30 AM CST)Fishbowl API
MercadoLibreSalesImportJobImports prior-day orders from Mercado Libre marketplace.Daily at 8:00 (2:00 AM CST)Mercado Libre API
UpdateSalesChannelJobCross-references SalesImport to ProductSalesOrder.ChannelName via ChannelOrderID and the CA temp import file.Daily at 8:30 (2:30 AM CST)SalesImport table
SalesInventoryUpdateJobMatches sales rows (Fishbowl + Mercado Libre) to WorkTracking units by serial (FIFO); sets UnitSoldDate / UnitShippedDate / station; links SalesImportLineItemDetail.WorkTrackingID. ⚠️ Hardcoded facility → tenant mapping (MX/CA → Mexicali, SL → Dallas SL).Daily at 9:00 (3:00 AM CST)Fishbowl + Mercado Libre + WorkTracking
JobPurposeTrigger
OemProductDownloadJobFetch OEM product data by SKU.Manual (no cron)
JobStatusReason
ConvertSalesImportJobStubMercado Libre → ProductSalesOrder conversion incomplete (commented-out body); currency / bundle handling deferred
TranslateEngineeringDataEntriesJobStubConverts EngineeringData rows → SystemInformation; mapper logic incomplete; sets SysInfoConversionFailed on error
PurgeAuditLogJobDisabledComment at line 47: "REMOVED IN FAVOR OF DIRECT INSERT TO MONGO". SQL AuditLog table retention now handled by Mongo TTL (⚠️ confirm SQL table truly receives no writes).

Consolidated from inspection of WebAPP/Jobs/*:

SmellWhereRisk
Bare catch {}AssignSystemVarianceCodeToNewUnitsJob:36, TranslateSystemInformationEntriesJob:36, BaseJob.EmailDevelopersOnError():60Silent failures — operations may report success while doing nothing
Hardcoded tenant IDsSyncSalesAndInventoryWithWorkTrackingJob:14, GenerateAndNotifyOfHpCoaReportJob:56 (TODO comments flag)New tenant onboarding requires code change
Hardcoded facility → tenant mappingSalesInventoryUpdateJob:115–126Same as above; fragile string matching (MX, CA, SL)
No idempotency gatingFishbowlSalesImportJobRelies on external ref dedup; concurrent runs could double-import
No retry / circuit breakerAll external API calls (BitRaser, Fishbowl, Mercado Libre, HP PCB)Single API blip = job fails for the day
Per-order swallowFishbowlSalesImportJob:123Errors on individual orders logged but not retried
Silent skip on missing dataSalesInventoryUpdateJob.UpdateLineItemDetailAndWorkTracking:210Returns when WorkTracking not found; no surfacing
Incomplete stubs in production codebaseConvertSalesImportJob, TranslateEngineeringDataEntriesJobConfusing — appear to be wired but do nothing useful

  • Direction: primarily inbound (read) with one outbound sync.
  • Inbound: sales orders, sales-order line items, sales-order line-item details (serial numbers + facility/location), parts inventory.
  • Outbound: the always-on SyncSalesAndInventoryWithWorkTrackingJob writes inventory updates back to Fishbowl for the two hardcoded tenants.
  • Wiring: FishbowlDbContext (Pomelo MySQL, NoTracking globally), FishBowlInventoryIntegrationService, IFishbowlService (contracts).
  • Cadence: see job table above.
  • Direction: inbound only.
  • Inbound: marketplace orders.
  • Wiring: IMercadoLibreService (Services.WebAppServicesImplementation.Domain).
  • Cadence: daily.
  • Direction: inbound, read-only.
  • What it does: product autocomplete, catalog, product info, tech specs, images by HP model.
  • Wiring: Services.HPPCBApi.HPPCBApiService, named HttpClient "pcb".
  • Trigger: ad-hoc (from OemProductService, OEM lookup flows).
  • Direction: inbound, read-only.
  • What it does: retrieves drive-erasure and verification reports for the past day.
  • Wiring: IBitraserClient.
  • Persistence: DriveEraseLog, DriveVerificationLog tables + JSON copy in Azure Blob.
  • Direction: inbound, file-based.
  • What it does: provides ChannelName enrichment to reconcile against SalesImport.SiteOrderID.
  • Wiring: UpdateSalesChannelJob reads the file and updates ProductSalesOrder.ChannelName.
  • Direction: inbound, push.
  • What partners do: submit inbound orders, query shipment status.
  • Wiring: CTLShopFloor.Presentation.ExternalAPI with ApiKeyAuthMiddleware + BearerTokenAuthMiddleware; custom ExternalApiMultiTenantStrategy.
  • Models: CreateNewInboundOrderRequest, CreateNewOutboundOrderRequest.
  • Controllers: AuthController, InboundOrderController, TestController.
  • Direction: bidirectional (offline).
  • Inbound: unit physically moves there from the Image Download station.
  • Outbound: uploads WindowsTestResultsUpload, populates SystemInformation + SystemInformationDisk + SystemInformationGraphic + 8 Engineering* tables; sets WorkTracking.SystemInformationBlob and WindowsTestResultBlob; injects DigitalProductKey and sets AssignedSerialNumber + InjectionTime.
  • Direction: outbound (writes) + inbound (reads for downloads).
  • Containers observed:
    • trackingreport{tenantname} — 6-month tracking reports (daily, Excel).
    • blobstorage (data-protection keys for ASP.NET Identity).
    • Drive erasure JSON dumps (BitRaser job, lines 75, 127).
    • Image / PDF uploads — managed via AzureBlobContainersControllerService + AzureBlobStorageContainers entity.
  • Cleanup: the tracking-report job deletes blobs older than 14 days.
  • Direction: outbound.
  • What: every entity change via Audit.NET interceptor.
  • Database: auditLog
  • Collection: shopFloor
  • Ignored entities: AuditLog, SalesImport
  • Ignored fields: WorkTracking.SystemInformationBlob, WorkTracking.WindowsTestResultBlob (too large)
  • Note: the SQL AuditLog table still exists; ⚠️ confirm no writes still hit it.
  • Direction: outbound.
  • What: all Serilog events at minimum level Verbose.
  • Used by: every Serilog-enabled component.

Two parallel email pipelines:

  • IEmailSenderEmailSender (custom implementation, registered Scoped in Program.cs line 55).
  • Per-project SMTP via EmailServerSettings: SMTP_Server, Port, FromAddress, UserName (encrypted password), Security (TLS/SSL), Active flag.
  • Sending is gated by EmailServerSettings.Active per project.
  • Notification enum (13 types) defines alert categories.
  • NotificationRecipient rows hold destination emails per notification type.
  • Jobs query notificationRecipientRepository.FindByCondition(r => r.NotificationID == (int)Notifications.X) and email each Destination.

Notification types observed:

IDNameSource
1ProductEOLNotifyUsersOfProductEOLJob
2EnrollmentTestResults⚠️ source not located
3DpkInventoryReport⚠️ source not located
4UnitsByStationType⚠️ source not located
5HpCoaReportGenerateAndNotifyOfHpCoaReportJob
11WorkOrderAcquisitionCostWorkOrderAcquisitionCostJob
12NewModelBOMHeaderRMA out-of-system path when a new model is auto-created
13DeveloperAlertBaseJob.EmailDevelopersOnError()

(Other enum values exist but no observed sources — possible dead code.)

EmailServerSettings.ProjectID plus Active filter means each project can have its own SMTP. The BaseJob and BitraserLogDownloadJob (line 147) both look up SMTP per-tenant.


⚠️ No in-app notification system observed. Notification + NotificationRecipient are an email-distribution-list mechanism, not a UI notification queue. Confirm before designing a UI notification feature on top of them.


PDFControllerService exists with methods for fetching pallets (internal / transfer / box / inbound) — likely for label and slip generation. Exact PDF rendering logic was not fully traced. Likely outputs:

  • Pallet labels (barcode + contents)
  • Bill of Lading slips (WorkOrders.BOLFileIdentifier linkage)
  • Inbound / outbound packing slips

⚠️ Confirm whether PDF rendering uses Syncfusion or a separate library.


  • WebAPP request size limit: 1 GB (Program.cs lines 18, 57–69) — generous, used for pre-alert Excel and image uploads.
  • Pre-alert Excel parsing: RmaForOutOfSystemUnitsControllerService.ProcessFile (lines 279–386) — also auto-creates BOMHeader rows for unknown SKUs and emails admins.
  • General file upload mapping: FileUploadMappingService.

This is the most complex job chain. Documenting it explicitly because the order matters.

sequenceDiagram
participant FB as Fishbowl
participant ML as Mercado Libre
participant CA as Channel Advisor temp file
participant DB as Database
participant J1 as FishbowlSalesImportJob<br/>1:30 AM CST
participant J2 as MercadoLibreSalesImportJob<br/>2:00 AM CST
participant J3 as UpdateSalesChannelJob<br/>2:30 AM CST
participant J4 as SalesInventoryUpdateJob<br/>3:00 AM CST
FB->>J1: prior-day orders
J1->>DB: ProductSalesOrder + line items + details
ML->>J2: prior-day orders
J2->>DB: ProductSalesOrder + line items + details
CA->>J3: temp file
J3->>DB: ProductSalesOrder.ChannelName updated
DB->>J4: SalesImport rows
J4->>DB: Match by serial → WorkTracking.UnitSoldDate / UnitShippedDate / SentToSalesDate; SalesImportLineItemDetail.WorkTrackingID

After this chain runs, every sold unit’s WorkTracking row should reflect its disposition. If WorkTracking was not found for a serial, J4 logs and continues (line 210) — these go to a “couldn’t reconcile” silent failure.

🔧 Refactor finding: silent failures in the sales pipeline are an audit gap. There should at minimum be a SalesImportReconciliationFailures table or alert.


Two distinct API surfaces:

AspectPresentation.ExternalAPIPresentation.WebAPI
AudiencePartner systemsInternal admin, imaging stations, OEM lookup
AuthAPI Key + BearerAPI Key only
Tenant resolutionCustom ExternalApiMultiTenantStrategyStandard header / session / route
ControllersAuthController, InboundOrderController, TestControllerBomHeaderController, DigitalProductKeysController, ImagingStationController, LoginController, OemProductController, ProjectsController, TenantsController, UserController, …
PurposePush inbound orders, query shipment statusInternal data access, admin ops, imaging-station data

Config flagEffect on jobs/integrations
JOBS:allowJobsExecutionGates Hangfire registration. Must be false on dev machines.
Migrations:allowMigrationsGates EF migration application. Independent of jobs.
ConnectionStrings:RepositoryDbPrimary SQL Server.
ConnectionStrings:TenantDbTenant registry SQL Server.
ConnectionStrings:FishbowlDbFishbowl MySQL.
ConnectionStrings:AuditLog (or similar)MongoDB for audit.
ConnectionStrings:blobstorageAzure Blob.
AppSettings:TokenJWT signing secret. ⚠️ Currently in appsettings.json — see security findings in 10-architecture.md §7.
SlowQueryThresholdEF Core slow-query log threshold (default 5000 ms).