Skip to content

BC Integration Worker

The BC Integration Worker (BCIntegration.Worker) is a .NET 9 hosted Windows Service. It runs on a 5-minute loop and does three kinds of work:

  1. Inbound from BC — payment application data and ledger entries (read-only API calls into BC, writes into SMART SQL).
  2. Outbound to BC — drains TransmissionMaster rows with Status = 'N' and posts custom API entities in Business Central.
  3. Status bookkeeping — sets TransmissionMaster.Status to C (completed) or E (error) with ReturnMessage / ReturnCode.

SMART desktop and web apps enqueue work by inserting into TransmissionMaster and the child Transmission* tables below. This worker is the component that actually talks to BC.

For the business picture of transmissions across all SMART apps, see How SMART talks to Business Central. This page is the technical reference for what this worker implements today (source: Worker.cs, Api/ApiService.cs on main).

  • 15 TransmissionMaster.TransType codes are processed (lot, vend, … cpcl, soap).
  • 14 child Transmission* tables are read (plus TransmissionPaymentStatus on inbound pays).
  • TransmissionPOGenerate and TransmissionLandDevBudgetActuals exist in the Core project but are not used by the worker.
  • Ledger mirror tables BCJobLedger and BCGeneralLedger are filled from BC; they are not transmission queues.
  • OAuth2 client-credentials against login.microsoftonline.com; company-scoped OData under ApiSettings:BaseUrl.

When Sync:BCLedgersOnly is false (production default), each cycle runs in order:

StepWhat happens
1Payment status pull (pays) — For each calendar day not yet marked, GET BC projectAppliedEntries for yesterday (relative to Eastern time). Insert TransmissionMaster (TransType = pays, Status = N or C) and child TransmissionPaymentStatus rows when BC returned data.
2BC job ledger sync — GET /jobLedgers in 5,000-entry windows; insert into BCJobLedger where PostingDate > 2025-12-31.
3BC general ledger sync — GET /glEntries for entry numbers that exist on BCJobLedger but not yet on BCGeneralLedger (also after cutoff date).
4Transmission drainSELECT * FROM TransmissionMaster WHERE Status = 'N' AND TransType IN (...) — process each row (see catalog below).

While attached to a console:

  • Ctrl + Shift + P — pause before the next transmission.
  • Space — resume.
CodeMeaning in this worker
NQueued; will be picked up on the next drain pass.
CCompleted — BC call succeeded (or pays day had no rows / payment update finished). DateSent set.
EError — see ReturnMessage, ReturnCode; may also log to TransmissionMasterErrorLog.
HHold — not selected by the worker (Status = 'N' filter only).

Other SMART status codes (X, R, q, …) are documented on Transmissions.

ModeHTTPWhen used
APOSTNew entity in BC.
UPUTUpdate existing entity.
DDELETERemove entity (supported in API layer; uncommon in queue).

For comm, lot, vend, vetp, and vadr, the worker may flip AU before posting: it GETs the BC record first (ResolveAddOrUpdateAsync). Batch types esti and acgr do the same GET-per-row inside ProcessBatchWithTransmissionAsync.


Only these SQL tables participate in the worker loop. Other Transmission* tables in SmithDouglasCommunities may be filled by SMART for other processes; they are out of scope for this service unless added to Worker.cs.

Child tableTransTypeDirectionBC API (custom publisher)
TransmissionMaster(all)Control
TransmissionCommunitycommSMART → BC/dimensionValues (COMMUNITY)
TransmissionLotlotSMART → BC/dimensionValues (LOT), then /projects, /defaultDimensions on add
TransmissionDivisiondiviSMART → BC/dimensionValues (DIVISION)
TransmissionVendorvendSMART → BC/vendors
TransmissionVendorTypevetpSMART → BC/vendorPostingGroups
TransmissionVendorAddressvadrSMART → BC/vendorRemitAddresses
TransmissionTermtermSMART → BC/paymentTerms
TransmissionPOApprovepoap, soapSMART → BC/unpostedPurchaseDocs + /unpostedPurchaseDocLines
TransmissionContractPostClosingcpclSMART → BC/postClosingJobGLJrnlLines (one POST per amount column)
TransmissionInvoiceSubmissioninv1SMART → BC/unpostedPurchaseDocs + /unpostedPurchaseDocLines
TransmissionEstimateestiSMART → BC/projects, /projectTasks
TransmissionLotActivityGroupacgrSMART → BC/projects, /projectTasks
TransmissionLotStatuslstsSMART → BCOData /$batch PUT on LOT dimensionValues
TransmissionPaymentStatuspaysBC → SMARTGET /projectAppliedEntries (inbound); no BC POST on drain
ArtifactNotes
TransmissionPOGenerateModel exists; no case in Worker.cs.
TransmissionLandDevBudgetActualsModel exists; no handler in Worker.cs.
All other Transmission* tablesLegacy or unused by this worker — not listed on Transmissions.
TableSourcePurpose
BCJobLedgerGET /jobLedgersLocal copy of BC project ledger entries after cutoff.
BCGeneralLedgerGET /glEntriesG/L lines linked to job ledger via LedgerEntryNo / EntryNo.

Cutoff constant in code: BCLedgerSyncStartDate = 2025-12-31. Entries on or before that date are not backfilled.


Not created by SMART users directly. The worker creates one TransmissionMaster per calendar day being reconciled (Eastern time), starting the day after the last pays DateAdded.

GET {BaseUrl}/projectAppliedEntries with filter:

  • systemModifiedAt on the target calendar day (BC “yesterday” relative to the run).
  • $select: open, documentNo, postingDate, appliedDocNo, projectNo, projectTaskNo.

Mapped into TransmissionPaymentStatus:

BC fieldSMART columnUse
documentNoDocumentNoTreated as PO number (must parse as integer for update).
postingDatePostingDateCheck date written to SMART.
appliedDocNoAppliedDocNoCheck / application document number.
systemModifiedAtSystemModifiedAtAudit from BC.
projectNoProjectNoLot/project key from BC.
projectTaskNoProjectTaskNoTask on project.
openOpenLoaded from API (not used in bulk update filter).
  1. TransmissionMasterCompanyID = SGW, TransType = pays, Mode = A. Status = C if BC returned no rows (day marked done); Status = N if there are rows to process.
  2. TransmissionPaymentStatus — bulk insert when data exists.
  3. On drain (case pays): PermanentOrderLog updated — CheckDate = PostingDate, CheckNumber = AppliedDocNo where PONumber = DocumentNo (rows with valid integer PO and non-null AppliedDocNo only).
  4. TransmissionMaster set to C — no outbound BC API call.

For each type: SMART child row(s)BC API payload highlights. Unless noted, success sets TransmissionMaster to C.

TableTransmissionCommunity
ModesA / U / D; may auto-switch to U after GET
Sent to BCPOST/PUT /dimensionValues: dimensionCode = COMMUNITY, code = CommunityID, name = Description
Key SMART columnsCommunityID, Description (also Subaccount, Status, CommunityName, AreaID on row — not all sent in API body)
ReceivedNone
TableTransmissionLot
ModesA / U / D; may auto-switch to U after GET
Sent to BCPOST/PUT /dimensionValues: dimensionCode = LOT, code = formatted lot (CCC-BBB-UUU from 12-char LotAccountCode), name = ProjectDesc
On A only (extra calls)POST /projects if missing (no, sellToCustomerNo, dimensions). PUT /defaultDimensions for LOT, COMMUNITY, DIVISION on table 167 (skipped for placeholder lots ending in XXXXXXX).
Key SMART columnsLotAccountCode, ProjectDesc, ProjectID, BuildingID, UnitID, address fields, status columns (APStatus, ARStatus, …) — dimension sync uses account code + description
ReceivedNone
TableTransmissionDivision
ModesA / U / D
Sent to BCPOST/PUT/DELETE /dimensionValues: dimensionCode = DIVISION, code = DivisionID, name = DivisionName
Key SMART columnsDivisionID, DivisionName, Description, Status
ReceivedNone

PerformPostAsync / PerformPutAsync implement TransmissionDivision, but ProcessTransmission<T>’s type guard does not list TransmissionDivision — verify in your build that divi rows are not logging “Unsupported type” before relying on this path.

TableTransmissionVendor (+ join to Term, Vendor, vwUserAccount for DueTypeID, EmailAddress)
ModesA / U / D; may auto-switch to U after GET on /vendors('{AccountingPackageVendorID}')
Sent to BCPOST/PUT /vendors: no, name, address, phoneNo, telexNo (fax), vendorPostingGroup, paymentTermsCode, postCode, state, blocked (from StatusName: All / Payment / not blocked)
Key SMART columnsAccountingPackageVendorID, VendorName, VendorTypeID, TermID, StatusName, address/remittance fields
ReceivedNone
TableTransmissionVendorType
ModesA / U / D; may auto-switch to U
Sent to BCPOST/PUT /vendorPostingGroups: code = VendorTypeID, description
Key SMART columnsVendorTypeID, Description, Status
ReceivedNone
TableTransmissionVendorAddress
ModesA / U / D; may auto-switch to U
Sent to BCPOST/PUT /vendorRemitAddresses: vendorNo, code (REMIT / 1099 / DEFAULT from AddressTypeID R/T/other), address lines
Key SMART columnsAccountingPackageVendorID, AddressTypeID, Address1, Address2, City, State, Zipcode
ReceivedNone
TableTransmissionTerm
ModesA / U / D (no GET flip in worker)
Sent to BCPOST/PUT /paymentTerms: code = TermID, description = TermName, dueDateCalculation = DueInterval + 'D', discountPerc
Key SMART columnsTermID, TermName, DueInterval, DiscountPercentage, …
ReceivedNone

poap / soap — PO / service order approval (purchase documents)

Section titled “poap / soap — PO / service order approval (purchase documents)”

Both types use the same code path and TransmissionPOApprove child rows. soap is “service order complete”; poap is standard PO approval.

TableTransmissionPOApprove (one or many rows per master)
Processing order1) AddPOApproveHeader on first row. 2) POST each line to /unpostedPurchaseDocLines.
Header sent to BC (if document missing)POST /unpostedPurchaseDocs: no = PONumber, documentType Invoice or Credit Memo (from sign of InvoiceAmount), buyFromVendorNo / payToVendorNo, paymentTermsCode, dimension shortcuts, documentDate
Line sent to BCPOST /unpostedPurchaseDocLines: documentNo, type = G/L Account, no = AccountID, projectNo, projectTaskNo, quantity = 1, directUnitCost = abs(InvoiceAmount)
AccountIDResolved in SQL: PO starting with 1 → lot WIP account; 8 → variance account; 9 → service request type account; else default 14300
Key SMART columnsPONumber, InvoiceAmount, InvoiceDate, AccountingPackageVendorID, LotAccountCode, TaskID, CompanyID, ProjectID, BuildingID, UnitID
ReceivedNone
ValidationMissing AccountingPackageVendorID → master E without calling BC

inv1 — Invoice approval (land dev / AP invoice submission)

Section titled “inv1 — Invoice approval (land dev / AP invoice submission)”
TableTransmissionInvoiceSubmission (one or many lines per invoice)
Processing order1) AddInvoiceSubmissionHeader on first row. 2) POST each line.
Header sent to BCPOST /unpostedPurchaseDocs when missing: no = InvoiceNumber, vendor, terms, dimensions, documentDate
Line sent to BCPOST /unpostedPurchaseDocLines: documentNo = InvoiceNumber, no = AccountID, projectNo, projectTaskNo, directUnitCost = abs(TransactionAmount)
Key SMART columnsInvoiceNumber, TransactionAmount, AccountID, LotAccountCode, TaskID, AccountingPackageVendorID, InvoiceDate, CompanyID, TermID
ReceivedNone
TableTransmissionContractPostClosing (single row; joined to ContractPostClosing, Contract, Community, Company, user name)
ModesUses master Mode on each line POST
Sent to BCOne POST per non-zero amount field to /postClosingJobGLJrnlLines: documentNo = ContractSysID, postingDate = ClosedDate, accountNo (G/L or bank), amount (sign rules per column), jobNo / jobTaskNo / dimensions vary by column (e.g. CashToSeller clears project; WorkInProgress uses task JOURN; land dev columns use D1015 / CLOTC)
Amount columnsPlanPrice, OptionRevenue, LotPremium, PriceAdj, CashToSeller, WIPLandDevDebit/Credit, LandDevelopmentLotCost, ConstructionCost, DepositReceived, closing costs, warranty, concessions, etc. (see AccountToColumns in ApiService.cs)
Key SMART columnsContractSysID, ClosedDate, ClosingDesc, PostClosingBankAcct, CommunityID, financial columns on ContractPostClosing
ReceivedNone
TableTransmissionEstimate (many rows per master)
Per rowGET /projectTasks; if missing → A, else U. AddProject first. POST/PUT /projectTasks: projectNo, projectTaskNo, description = TaskDesc, originalBudget = BudgetAmount
Key SMART columnsLotAccountCode, TaskID, TaskDesc, BudgetAmount, ProjectID, CommunityID, CompanyID
ReceivedNone

acgr — Lot activity groups (project tasks)

Section titled “acgr — Lot activity groups (project tasks)”
TableTransmissionLotActivityGroup (many rows)
Per rowEnsure /projects exists (POST if GET fails). POST/PUT /projectTasks: projectTaskNo = TaskID, description = TaskName
Key SMART columnsLotAccountCode, TaskID, TaskName, GroupsID, CompanyID
ReceivedNone
TableTransmissionLotStatus (many rows per master)
Sent to BCSingle OData batch (POST {RootUrl}/$batch) of PUTs to dimensionValues('LOT','{community-building-unit}') with body { lotstatus: BCLotStatus }
Key SMART columnsProjectID, BuildingID, UnitID, LotStatusID, BCLotStatus
ReceivedNone
CompletionEntire batch must return HTTP 200 per sub-request; otherwise master → E

APIGET /jobLedgers?$filter=entryNo gt {cursor} and entryNo le {cursor+5000} and PostingDate gt 2025-12-31
Stored fieldsJobNo, PostingDate, JobTaskNo, EntryNo, DocumentNo, Description, DocumentDate, LedgerEntryNo, TotalCost, No
CursorMAX(EntryNo) in BCJobLedger; bootstrap uses first BC entry after cutoff
APIGET /glEntries with same windowing on entryNo
Stored fieldsEntryNo, PostingDate, GLAccountNo, DocumentNo, Description, Amount, Debit, Credit, DocumentDate, ExternalDocumentNo, SourceNo, ShortcutDimension3Code
ScopeOnly inserts GL rows whose EntryNo matches a BCJobLedger.LedgerEntryNo not yet present in BCGeneralLedger (excludes LedgerEntryNo = 0 migration rows)

BCIntegration/BCIntegration.Worker/appsettings.json:

{
"ConnectionStrings": {
"DefaultConnection": "Server=...;Database=SmithDouglasCommunities;..."
},
"Database": {
"LedgerQueryCommandTimeoutSeconds": 120
},
"ApiSettings": {
"BaseUrl": "https://api.businesscentral.dynamics.com/v2.0/{tenant}/{environment}/api/{publisher}/{group}/v2.0/companies({companyGuid})",
"RootUrl": "https://api.businesscentral.dynamics.com/v2.0/{tenant}/{environment}/api/{publisher}/{group}/v2.0/",
"TokenEndpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token",
"ClientID": "...",
"ClientSecret": "...",
"TenantID": "...",
"CompanyGUID": "...",
"EnvironmentName": "Production",
"Scope": "https://api.businesscentral.dynamics.com/.default",
"APIPublisher": "...",
"APIGroup": "...",
"GrantType": "client_credentials"
},
"Sync": {
"BCLedgersOnly": false,
"RunOnce": false
}
}

Authentication: OAuth2 client credentials (grant_type from config). All company API calls use BaseUrl; OData batch (lsts) uses RootUrl + companies({CompanyGUID}).

Logging: Serilog → console and C:\Logs\BCIntegration.log (daily rolling).


BCIntegration/
├── BCIntegration.sln
├── BCIntegration.Core/
│ ├── Interfaces/ IApiService, IDatabaseService
│ └── Models/ Transmission* + BCJobLedger, BCGeneralLedger
├── BCIntegration.Infrastructure/
│ ├── Api/ApiService.cs BC REST mapping
│ └── Data/DatabaseService.cs
├── BCIntegration.Worker/
│ ├── Program.cs
│ ├── Worker.cs main loop
│ └── appsettings.json
└── scripts/ publish-win-x64.ps1, build manifest

  1. Find TransmissionMaster with Status IN ('N','E') for the business event.
  2. Join the child table from the catalog above on TransmissionMasterSysID.
  3. For E, read ReturnMessage (often BC OData error JSON).
  4. Confirm the worker service is running and not stuck in BCLedgersOnly mode.
  1. Locate TransmissionMaster where TransType = pays for the date.
  2. If Status = C but checks wrong, inspect TransmissionPaymentStatus rows (DocumentNo must be numeric PO; AppliedDocNo required).
  3. Verify PermanentOrderLog for that PONumber.
  • Check BCJobLedger max EntryNo vs BC UI.
  • Entries on or before 2025-12-31 are intentionally skipped.
  • LedgerEntryNo = 0 rows are excluded from GL pairing.

Use console Ctrl+Shift+P and Space when running interactively; Windows Service deployments rely on log files only.