D365Library.Core 1.0.63.3

Create Sales Order

IWarehourseMgrServiceManager.CreateSalesOrder validates and handles Create Sales Order inside the manager. The method keeps its original name and parameters, but now returns ApiResponse<D365ServiceOperationData> instead of a string. This is a breaking API change: rebuild consumers and replace direct string usage with a success check followed by response.Data.Result. The additional validated operations listed below also use structured responses.

The current version is D365Library.Core 1.0.63. Consumers keep registering services.AddD365LibraryServices("Uat") and calling CreateSalesOrder. Only response handling changes. SQL tracking, connection selection and retry policy registration are automatic inside the library; no additional appsettings, connection string, options or AddD365Tracking call is needed in the consuming app. This candidate has not been published.

var manager = serviceProvider.GetRequiredService<IWarehourseMgrServiceManager>();
var response = manager.CreateSalesOrder(
    "TSPM", salesTable.ToArray(), salesLine.ToArray());

if (response.Success) {
    Console.WriteLine(response.Data.Result);
} else {
    Console.WriteLine($"{response.Data.ErrorCode}: {response.Message}");
    Console.WriteLine(response.Error);
    Console.WriteLine(response.Data.CorrelationId);
}

The return type is ApiResponse<D365ServiceOperationData> in D365Library.Models, shared with PostInvoice, PostPackingSlip, CreateCustomerAsync and UpdateCustomer: Success, Message, Data, UTC Timestamp, original Error, and StatusCode. StatusCode is metadata; an HTTP controller must set its HTTP response status itself. Data contains the operation name, order ID, numeric error code, correlation ID, actual SOAP attempt count, retry exhaustion, outcome uncertainty, logs, attempt failures, storage failures, and any retry safety check error. Attempt count is zero for local validation failures. A correlation ID links attempts; it is not an idempotency key and is not sent to D365 as one.

Other validated operations

All five operations use D365ServiceOperationSoapTransport, the shared response reader, result validators and SQL retry checks in D365Library.Operations. ID365ServiceOperationTransport includes the narrow ICreateSalesOrderTransport contract for compatibility; both resolve to the same scoped transport by default. Custom transport implementations of the common interface also implement Create. Operation-specific validation and retry decisions remain in their own methods.

PostInvoice, PostPackingSlip, CreateCustomerAsync and UpdateCustomer keep their existing names and parameters and return ApiResponse<D365ServiceOperationData> (Task<ApiResponse<D365ServiceOperationData>> for CreateCustomerAsync). Their original successful string is now Data.Result:

var packing = manager.PostPackingSlip(company, salesId, pickingList);
if (!packing.Success) { Console.WriteLine(packing.Error); return; }
var invoice = manager.PostInvoice(company, packing.Data.Result);
if (invoice.Success) Console.WriteLine(invoice.Data.Result);

No new host settings are required. Common response metadata includes ErrorCode, CorrelationId, Attempts, Failures, StorageErrors and AppliedRetryPolicy. Infolog errors override successful-looking IDs. UpdateCustomer also accepts explicit true/ok/success acknowledgements. Other operations require an identifier; CreateCustomerAsync also accepts comma/semicolon-separated customer identifiers. Null optional address/contact lists are sent as empty arrays; null entries are validation errors. Null/empty customer or update lists are validation errors.

When an enabled error rule classifies a CreateCustomerAsync failure as 1003 (CustomerAlreadyExists), it looks up each requested AccountNum using GetCustomerByCustomerCode in the same company. If all accounts exist, it returns success with the existing accounts in Data.Result, even if retries are disabled or the last attempt has been reached. The original failure remains in Data.Failures. If a single account is not found, normal retry policy checks apply using code 1003, attempt limits and delays. Apply D365Library/Tracking/Database/006_AddCustomerAlreadyExistsRule.sql after scripts 003 and 005 to seed the error rule and retry policy for TOne/Uat (adjust the script scope for other applications/environments). The rule matches The record already exists. at priority 5, before the default SQL rules. Admins can edit its Pattern/MatchType or disable it in D365ErrorRule; the manager checks the classified code rather than message text. Without an enabled matching rule, this reconciliation does not run. Its code-1003 retry policy must also be enabled to resend when the account is not found. Existing admin rows are retained. Missing accounts, failed lookups and partially committed batches do not authorize a resend. Custom service transports can implement ID365CustomerLookupTransport to support this lookup without changing ID365ServiceOperationTransport.

Library owners apply D365Library/Tracking/Database/004_AddServiceRetryPolicies.sql to the central tracking database after script 003. Rules use the public operation names, including CreateCustomerAsync. Existing admin policies are preserved. Missing rules stop resends. Rollback deadlocks can retry; ambiguous SQL errors/timeouts cannot. A single customer insert rejection can retry, but customer batches are never replayed automatically. Each operation has its own CorrelationId.

Error codes

All five validated operations first evaluate enabled rows in d365tracking.D365ErrorRule. Admins can add new response/error patterns without republishing NuGet after installing preview.9. Rules use Contains, Equals or StartsWith (case-insensitive), scoped by application, environment and operation or *, ordered by Priority then Id ascending. Description becomes Message and the original text remains in Error; no match keeps the built-in codes below. Library owners can use Tracking/Database/005_CreateD365ErrorRule.sql to set up the table. Consumers need no new configuration.

Data.AppliedErrorRule and failure snapshots identify the matched row; Data.ErrorRuleCheckError reports lookup problems. A lookup problem preserves the ERP outcome and stops retries. A classification rule does not grant permission to resend: D365RetryPolicy and operation-specific replay safety still apply. See SQL tracking documentation for editing rules and monitoring their effect.

Code Meaning StatusCode
0 Success 200
1001 Invalid input or retry settings 400
1002 D365 business error 422
1003 Customer already exists (configured error rule); verify by account 422
1101 SQL error, cause not established 500
1102 SQL deadlock victim reported 500
1201 Authentication failed 401
1202 Access denied 403
1301 Timeout 504
1302 Connection error 503
1401 Empty response 500
1402 Unrecognized result format 500
1501 Unclassified SOAP fault 500
1901 Error storage failed (separate Data.StorageErrors) Does not replace create status
1999 Unexpected error 500

The reported Cannot create a record in Sales orders (SalesTable)... The SQL database has issued an error is 1101, whether returned in the body, Infolog, or a typed SOAP fault. SQL classification uses the available D365 text; 1101 does not prove the failure is transient. Fault request ID and exception type are preserved in Data.Failures. Business validation rules remain on D365.

A successful body must contain a single sales order ID without error Infolog entries. The default ID format accepts ASCII letters, digits, dots, underscores, slashes and hyphens, excluding standalone status words. Confirm the actual D365 success contract in UAT; override CreateSalesOrderOptions.IsSalesOrderId if your service returns a different ID format. Unknown text is 1402.

Retry configuration

CreateSalesOrder always enables the retry pipeline. Host EnableRetry=false is ignored for this operation. The library supplies its own default safety check. It reads D365RetryPolicy after each failure using the internal application and environment profile. Admins control Enabled, MaxAttempts and DelaySeconds in that table; consumers do not configure retries. A disabled or missing rule stops resending. Database/policy errors preserve the original ERP error and stop retry.

The built-in check supports the explicit SQL rejection Cannot create a record in Sales orders (SalesTable) and SQL deadlock messages that identify a victim/rollback. Ambiguous SQL errors, timeouts and missing replies are not resent automatically. A matching SQL rule alone does not establish idempotency. Independently repeated calls can still create separate orders.

Policy limits allow 1-10 total sends, including the original call. Delays are exponential and capped at 300 seconds. Each send uses a fresh SOAP channel. Data.AppliedRetryPolicy, Attempts, Failures, RetryExhausted and the shared CorrelationId explain the final result. SQL tracking records each attempt and whether a later attempt succeeded. See D365Library/Tracking/README.md for admin SQL.

Error storage

Database monitoring is registered automatically by AddD365LibraryServices using internal UAT/Production connection profiles. The library owner manages these profiles in D365Library/Tracking/Configuration/DefaultTrackingProfiles.json and applies the schema once. Package consumers supply no tracking configuration. See D365Library/Tracking/README.md for administration. ID365OperationTracker receives lifecycle events independently of the error store below. A tracking failure appears in Data.StorageErrors with its stage and never causes another ERP create.

The default ID365OperationErrorStore submits failures to the application's configured Exceptionless client. Configure Exceptionless in the host. Submission does not guarantee durable delivery. For database persistence, implement ID365OperationErrorStore.Save(D365OperationFailure) in the consuming application:

services.AddScoped<ID365OperationErrorStore, MySalesOrderErrorStore>();
services.AddD365LibraryServices(configuration);

Operation tracking is persisted in SQL Server. The library does not include a local text or JSON Lines file store. Exceptionless remains the default additional diagnostic destination.

Each failure is saved before retry, with UTC timestamp, company, user, correlation ID, operation name, attempt number, code, original message, Infolog and SOAP fault details. Full sales payloads and credentials are not added to the error record. If storage throws, the failure remains in the returned response and storage code 1901 is reported separately. Storage failure never causes an additional create.

Tests

D365 tests are included in the existing TOne.Test.Services project in TOne/TOne.sln:

TOne/Tests/TOne.Test.Services/D365/
  Unit/CreateSalesOrderTests.cs
  Unit/D365OperationExecutorTests.cs
  Unit/D365TrackingTests.cs
  Unit/D365TrackingConfigurationTests.cs
  Unit/D365ScaffoldCompatibilityTests.cs
  Integration/CreateSalesOrderUatTests.cs
  Integration/UatFactAttribute.cs
  TestDoubles/

In Visual Studio, expand Tests > TOne.Test.Services > D365 > Unit to view the files. Build the test project and open Test Explorer for discovered cases. The former standalone D365 test project has been removed. Filter to this test namespace when running isolated tests; other existing TOne tests can call live external systems.

dotnet test TOne/Tests/TOne.Test.Services/TOne.Test.Services.csproj -p:GeneratePackageOnBuild=false --filter 'FullyQualifiedName~TOne.Test.Services.D365Tests&Category!=UAT'

The xUnit suite exercises the manager through mocked transport and error store, without D365 calls. For a controlled UAT integration test, retain the existing request fixture and change the call/assertions to:

var response = tools.CreateSalesOrder("TSPM", salesTable.ToArray(), salesLine.ToArray());
Assert.True(response.Success, response.Error);
Assert.Equal(0, response.Data.ErrorCode);
Assert.False(string.IsNullOrWhiteSpace(response.Data.Result));

UAT creates real records and must be run explicitly with appropriate test data. No UAT call or NuGet publication is part of the automated unit test suite.

Explicit UAT tests

Real-data integration tests live under TOne/Tests/TOne.Test.Services/D365/Integration:

File Real UAT operation and prerequisites
CreateSalesOrderUatTests.cs TSPM / SC02508 / 113-120-5008; success and nonexistent-customer rejection
PostPackingSlipUatTests.cs Creates a fresh TSPM / SC00303 / SER-IT3003 order, then posts its packing slip
PostInvoiceUatTests.cs Creates a fresh service-item order and packing slip, then posts its invoice
CreateCustomerUatTests.cs Creates a unique UT-prefixed customer in Go5 using existing DOM/THB/VAT/30D/Transfer master settings
UpdateCustomerUatTests.cs Creates a dedicated Go5 UAT customer, updates its name and reads it back

Sales fixtures reuse TemplateServiceTests master data, with quantity 1 and price 1.10. Every run generates a unique PO reference or test customer account. Posting tests create their own prerequisite documents and do not depend on another test's execution order. Customer tests use a test company name and no real contact email addresses. Review these master codes if UAT data changes.

All UAT cases are serialized in the D365 UAT test collection. Each step verifies the real SQL operation and attempt records by CorrelationId, including errors and successful retries, before continuing to the next write. Customer tests also read the customer back from D365. Results and diagnostics appear in Test Output; no local result/error files are written. The central SQL schema is checked before the first ERP write, and no schema or policy changes are applied by these tests.

Select an individual test in Visual Studio, or explicitly run, for example:

dotnet test TOne/Tests/TOne.Test.Services/TOne.Test.Services.csproj --no-restore -p:GeneratePackageOnBuild=false --filter 'Category=UAT&D365Operation=PostInvoice'

This command performs real UAT writes when the application environment below is Uat. Created records remain in UAT; tests do not delete or roll back posted documents. A failed later step can leave earlier successful documents in place. Use CorrelationId and Test Output to inspect those records before rerunning.

CreateSalesOrderUatTests uses the same TOneSetting:D365:EnvironmentName in TOne.API/appsettings.json that configures the application. It runs when that value is Uat and is skipped for other/missing environments. There is no separate test enable flag or test settings file. Rebuild to copy settings and refresh Visual Studio discovery:

{
  "TOneSetting": {
    "D365": {
      "EnvironmentName": "Uat"
    }
  }
}

The tests use the Uat configuration profile, rejects any host other than uat-aos.tks.co.th, requires HTTPS, and uses the library-owned retry pipeline. It uses the supplied TSPM fixture (customer SC02508 / item 113-120-5008). Review that fixture if UAT master data changes. The success test creates one real order; the invalid-customer test expects D365 business validation and verifies SQL Server operation and attempt records. Both cases require database tracking; neither writes result/error files.

The internal UAT tracking database and all three tracking tables must already exist. Tests check them before sending any ERP create request. No schema is created by tests.

Run the invalid-customer case first to confirm connectivity and error handling:

dotnet test TOne/Tests/TOne.Test.Services/TOne.Test.Services.csproj --no-restore -p:GeneratePackageOnBuild=false --filter 'Category=UAT&D365Operation=CreateSalesOrder&Scenario=InvalidCustomer'

After confirming that response, explicitly run the create case once:

dotnet test TOne/Tests/TOne.Test.Services/TOne.Test.Services.csproj --no-restore -p:GeneratePackageOnBuild=false --filter 'Category=UAT&D365Operation=CreateSalesOrder&Scenario=Success'

Results and attempt diagnostics are stored in SQL Server tables d365tracking.D365Operation and d365tracking.D365OperationAttempt, and the tests query them by the returned CorrelationId to verify persistence. The SQL-backed tracker replaces the former test file destination. Test output includes the CorrelationId, status, code, attempts and sales order ID for lookup. Historical file evidence listed in UAT-RESULTS.md remains unchanged; new tests do not update those files. A timeout or unknown outcome must be reconciled before rerunning the create case. These tests do not demonstrate SQL rollback or safe automatic retries; those require server-side evidence and a reliable unique request reference.

Adding another operation

The common execution pipeline lives in D365Library/Operations:

Component Responsibility
D365OperationExecutor Validation, attempts, retry safety, delays, response status and failure recording
D365OperationData Shared diagnostics inherited by each operation's result payload
D365OperationOptions<TRequest,TData> Typed options and reconciliation callback per operation
D365ErrorCodes / D365ErrorClassifier Shared numeric codes and SOAP/network exception mapping
ID365OperationErrorStore One storage integration for every operation, identified by Operation
D365SoapInvoker Authentication, a fresh channel per attempt, and safe channel cleanup

To implement another endpoint in this repository:

  1. Add a result class inheriting D365OperationData containing only its business fields, such as PurchaseOrderId. Return ApiResponse<ThatResult>.
  2. Define its options inheriting D365OperationOptions<ThatRequest,ThatResult>. The common base retries nothing by default. Choose allowed codes explicitly; never inherit sales-order retry rules implicitly.
  3. Add a transport using D365SoapInvoker with the correct client and service group. Register transport and typed options in ServiceConfiguration.
  4. Add the manager method in a partial file such as Tks2D365ServiceManager.PurchaseOrder.cs, and declare it on the relevant public interface. Supply a D365OperationDefinition and call the manager's shared operationExecutor.Execute.
  5. The definition supplies a unique operation name, input validation, transport call, response interpretation, success message, and existing-result validation and business-field copying. ReadResponse returns null on success or a D365OperationError on failure. Populate only business fields and current logs; the executor owns attempts, correlation IDs and failure history.
  6. For a different generated SOAP fault type, override the definition's ClassifyException delegate. Otherwise it uses the shared classifier, which recognizes the TKS service group's typed fault and common network exceptions.
  7. Add tests for that operation's validation, success contract, error mapping, safe retries and reconciliation. Do not copy the retry loop or error storage.

The numbers in D365ErrorCodes describe shared error categories and retain their existing values; they are not reserved solely for sales orders. Identify the operation with Data.Operation / D365OperationFailure.Operation. New business codes may be allocated separately without changing existing values.

Only CreateSalesOrder currently uses this pipeline. Other existing endpoint implementations are unchanged. Generic executor tests use an in-memory operation with a numeric record ID to verify reuse, independent retry policies, distinct operation identities, reconciliation and service-specific error classification.

Showing the top 20 packages that depend on D365Library.Core.

Packages Downloads
D365API
Package Description
7

Version Downloads Last updated
2.0.0-preview.9 8 10/27/2026
1.0.63.3 20 10/28/2026
1.0.63.2 4 10/27/2026
1.0.63.1 5 10/27/2026
1.0.62 37 08/25/2026
1.0.61 39 07/10/2026
1.0.60 47 07/08/2026
1.0.59 44 06/04/2026
1.0.58 55 04/23/2026
1.0.57 56 02/24/2026
1.0.56 3,464 02/13/2026
1.0.55 47 02/13/2026
1.0.54 42 02/12/2026
1.0.53 70 01/15/2026
1.0.43 67 04/29/2025
1.0.41 53 04/29/2025
1.0.40 46 04/29/2025
1.0.39 42 04/29/2025
1.0.38 53 04/29/2025
1.0.37 458 04/29/2025
1.0.36 51 04/29/2025
1.0.35 51 04/29/2025
1.0.33 963 04/29/2025
1.0.32 41 04/29/2025
1.0.31 54 04/29/2025
1.0.30 45 04/29/2025
1.0.29 37 04/29/2025
1.0.28 158 04/29/2025
1.0.27 43 04/29/2025
1.0.25 54 04/29/2025
1.0.24 53 04/29/2025
1.0.23 41 04/29/2025
1.0.22 52 04/29/2025
1.0.20 43 04/29/2025
1.0.18 54 04/29/2025
1.0.17 49 04/29/2025
1.0.16 40 04/29/2025
1.0.15 46 04/29/2025
1.0.14 52 04/29/2025
1.0.13 41 04/29/2025
1.0.11 42 04/29/2025
1.0.10 56 04/29/2025
1.0.4 45 04/29/2025
1.0.3 47 04/29/2025