Initial Enova365 sync service
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.dotnet/
|
||||
.packages/
|
||||
NuGet/
|
||||
**/bin/
|
||||
**/obj/
|
||||
*.user
|
||||
*.suo
|
||||
|
||||
# Lokalna konfiguracja wdrożeniowa; wzorcowa konfiguracja znajduje się w źródłach.
|
||||
release/config/enova.local.json
|
||||
5
Directory.Build.props
Normal file
5
Directory.Build.props
Normal file
@@ -0,0 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<NuGetAudit>false</NuGetAudit>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
10
NuGet.Config
Normal file
10
NuGet.Config
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
<config>
|
||||
<add key="globalPackagesFolder" value=".packages" />
|
||||
</config>
|
||||
</configuration>
|
||||
68
README.md
Normal file
68
README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# SmartB2B SYNC dla enova365
|
||||
|
||||
Dedykowana usługa Windows zastępująca proces Node na stanowiskach z enova365. Utrzymuje połączenie WAMP, dodaje zamówienia przez oficjalne API Enovy i wykonuje `eu.smartb2b.sql_raw` na osobnym połączeniu SQL.
|
||||
|
||||
## Najważniejsze właściwości
|
||||
|
||||
- Host nie zawiera bibliotek `Soneta.*`. Adapter ładuje je z katalogu `enova.installationPath` przy każdym starcie procesu.
|
||||
- Zmiana wersji lub katalogu Enovy wymaga restartu usługi. Start kończy się błędem, jeżeli nowy zestaw DLL nie jest zgodny z adapterem.
|
||||
- Operacje Enovy są wykonywane pojedynczo i w osobnych sesjach. Zapytania SQL korzystają z puli połączeń i mogą działać równolegle.
|
||||
- ZO jest zapisywane transakcyjnie, domyślnie do bufora. Powtarzający się numer obcy nie jest blokowany.
|
||||
- Pierwsza wersja obsługuje PLN, `price_netto` i rabat procentowy `0-100`.
|
||||
|
||||
## Konfiguracja
|
||||
|
||||
Plik `config/enova.json` zawiera portal WAMP, ścieżkę instalacji, alias bazy, operatora, definicję dokumentu, magazyn domyślny i tryb zapisu.
|
||||
|
||||
Sekrety nie są przechowywane w JSON-ie. Konto usługi musi widzieć dwie zmienne środowiskowe maszyny:
|
||||
|
||||
```powershell
|
||||
[Environment]::SetEnvironmentVariable('SMARTB2B_ENOVA_PASSWORD', '<hasło operatora>', 'Machine')
|
||||
[Environment]::SetEnvironmentVariable('SMARTB2B_SQL_CONNECTION_STRING', 'Server=...;Database=...;User ID=...;Password=...;TrustServerCertificate=True', 'Machine')
|
||||
```
|
||||
|
||||
Po zmianie zmiennych trzeba zrestartować usługę. Connection string powinien używać konta SQL o możliwie najmniejszych uprawnieniach; `sql_raw` celowo dopuszcza również polecenia modyfikujące dane dla zgodności z obecną usługą.
|
||||
|
||||
## Budowanie i testy
|
||||
|
||||
Z katalogu głównego repozytorium, przy zainstalowanym SDK .NET 8:
|
||||
|
||||
```powershell
|
||||
dotnet restore .\SmartB2B.EnovaSync.sln --configfile .\NuGet.Config
|
||||
dotnet build .\SmartB2B.EnovaSync.sln --no-restore
|
||||
dotnet run --project .\tests\SmartB2B.Enova.Tests --no-build --no-restore
|
||||
```
|
||||
|
||||
Ostrzeżenie `NU1603` dotyczące `Soneta.Generator` jest takie samo jak w działającym prototypie `test1` i nie blokuje kompilacji.
|
||||
|
||||
Kontrola konfiguracji i dynamicznego ładowania, bez zapisu do bazy:
|
||||
|
||||
```powershell
|
||||
.\src\SmartB2B.Enova.Service\bin\Debug\net8.0\SmartB2B.Enova.Service.exe --check-config
|
||||
```
|
||||
|
||||
## Publikacja i instalacja
|
||||
|
||||
```powershell
|
||||
.\publish.ps1
|
||||
```
|
||||
|
||||
Skrypt odtwarza katalog `release` z hostem, adapterem, konfiguracją i WinSW. Istniejąca konfiguracja `release/config/enova.json` jest zachowywana. Repozytorium zawiera również gotowy, skompilowany katalog `release`, więc na komputerze produkcyjnym nie trzeba instalować SDK. Przed instalacją:
|
||||
|
||||
1. Skopiuj katalog `release` na komputer produkcyjny i uzupełnij `release/config/enova.json`.
|
||||
2. Ustaw sekrety maszyny.
|
||||
3. Zatrzymaj starą usługę Node dla tego samego portalu WAMP.
|
||||
4. Skonfiguruj usługę do pracy pod dedykowanym kontem Windows, które potrafi uruchomić Enovę, widzi jej bazę i licencję.
|
||||
5. Jako administrator uruchom `release/daemon/install.cmd`.
|
||||
|
||||
Jeżeli usługa ma działać pod innym kontem niż `LocalSystem`, po instalacji ustaw je w `services.msc` albo poleceniem administracyjnym `sc.exe config smartb2bsync-enova obj= "DOMENA\\Użytkownik" password= "..."`, a następnie uruchom usługę ponownie.
|
||||
|
||||
Wycofanie polega na uruchomieniu `daemon/uninstall.cmd` i ponownym uruchomieniu dotychczasowej usługi Node. Nie ma migracji schematu bazy.
|
||||
|
||||
## Procedury WAMP
|
||||
|
||||
- `eu.smartb2b.place_order` przyjmuje `currency_iso`, `companyErpId`, `warehouseErpId`, `purchase_order_number`, `notes` i `lines[]` z polami `symbol`, `quantity`, `price_netto`, `discount`. Wynik jest zwracany jako kwargs: `order_erp_id`, `order_erp_symbol`, `value_netto`, `value_brutto`, `stocks`.
|
||||
- `eu.smartb2b.sql_raw` przyjmuje `query` i opcjonalne `params`; parametry odpowiadają kolejno `@1`, `@2`, itd. Wynik ma pola `recordsets`, `recordset`, `output`, `rowsAffected`.
|
||||
- Diagnostyka: `eu.smartb2b.sync.get_version`, `get_info`, `get_log`, `get_error_log`.
|
||||
|
||||
Kod produkcyjny, testy oraz gotowa wersja instalacyjna znajdują się w tym repozytorium.
|
||||
36
SmartB2B.EnovaSync.sln
Normal file
36
SmartB2B.EnovaSync.sln
Normal file
@@ -0,0 +1,36 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.11.35327.3
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartB2B.Enova.Contracts", "src\SmartB2B.Enova.Contracts\SmartB2B.Enova.Contracts.csproj", "{83EEA218-71A0-4E45-9572-B53767A3BC26}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartB2B.Enova.Adapter", "src\SmartB2B.Enova.Adapter\SmartB2B.Enova.Adapter.csproj", "{64C5439F-00AA-48F9-9A9B-DAE69DB0BC57}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartB2B.Enova.Service", "src\SmartB2B.Enova.Service\SmartB2B.Enova.Service.csproj", "{3473BD53-A2B9-46A8-AEE8-F087F373DF2B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartB2B.Enova.Tests", "tests\SmartB2B.Enova.Tests\SmartB2B.Enova.Tests.csproj", "{842D10FD-D5FC-4AF9-8D72-619BF6EE10C2}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{83EEA218-71A0-4E45-9572-B53767A3BC26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{83EEA218-71A0-4E45-9572-B53767A3BC26}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{83EEA218-71A0-4E45-9572-B53767A3BC26}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{83EEA218-71A0-4E45-9572-B53767A3BC26}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{64C5439F-00AA-48F9-9A9B-DAE69DB0BC57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{64C5439F-00AA-48F9-9A9B-DAE69DB0BC57}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{64C5439F-00AA-48F9-9A9B-DAE69DB0BC57}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{64C5439F-00AA-48F9-9A9B-DAE69DB0BC57}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3473BD53-A2B9-46A8-AEE8-F087F373DF2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3473BD53-A2B9-46A8-AEE8-F087F373DF2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3473BD53-A2B9-46A8-AEE8-F087F373DF2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3473BD53-A2B9-46A8-AEE8-F087F373DF2B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{842D10FD-D5FC-4AF9-8D72-619BF6EE10C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{842D10FD-D5FC-4AF9-8D72-619BF6EE10C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{842D10FD-D5FC-4AF9-8D72-619BF6EE10C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{842D10FD-D5FC-4AF9-8D72-619BF6EE10C2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
14
daemon/install.cmd
Normal file
14
daemon/install.cmd
Normal file
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
if not exist "smartb2bsync-enova.exe" (
|
||||
echo Brak smartb2bsync-enova.exe. Najpierw uruchom publish.ps1.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
smartb2bsync-enova.exe install
|
||||
if errorlevel 1 exit /b %errorlevel%
|
||||
|
||||
smartb2bsync-enova.exe start
|
||||
exit /b %errorlevel%
|
||||
BIN
daemon/smartb2bsync-enova.exe
Normal file
BIN
daemon/smartb2bsync-enova.exe
Normal file
Binary file not shown.
13
daemon/smartb2bsync-enova.xml
Normal file
13
daemon/smartb2bsync-enova.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<service>
|
||||
<id>smartb2bsync-enova</id>
|
||||
<name>SmartB2B SYNC - enova365</name>
|
||||
<description>Synchronizuje enova365 z portalem SmartB2B.</description>
|
||||
<executable>%BASE%\..\SmartB2B.Enova.Service.exe</executable>
|
||||
<workingdirectory>%BASE%\..</workingdirectory>
|
||||
<stoptimeout>30sec</stoptimeout>
|
||||
<log mode="roll-by-time">
|
||||
<pattern>yyyyMMdd</pattern>
|
||||
</log>
|
||||
<onfailure action="restart" delay="10 sec" />
|
||||
<delayedAutoStart />
|
||||
</service>
|
||||
7
daemon/uninstall.cmd
Normal file
7
daemon/uninstall.cmd
Normal file
@@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
smartb2bsync-enova.exe stop
|
||||
smartb2bsync-enova.exe uninstall
|
||||
exit /b %errorlevel%
|
||||
59
publish.ps1
Normal file
59
publish.ps1
Normal file
@@ -0,0 +1,59 @@
|
||||
param(
|
||||
[string]$OutputPath = (Join-Path $PSScriptRoot "release"),
|
||||
[ValidateSet("Debug", "Release")]
|
||||
[string]$Configuration = "Release"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$env:DOTNET_CLI_HOME = $PSScriptRoot
|
||||
$env:APPDATA = $PSScriptRoot
|
||||
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = "1"
|
||||
$env:DOTNET_CLI_TELEMETRY_OPTOUT = "1"
|
||||
|
||||
$bundledDotnet = Join-Path $PSScriptRoot "..\..\..\test1\.dotnet\dotnet.exe"
|
||||
$dotnet = if (Test-Path -LiteralPath $bundledDotnet) {
|
||||
(Resolve-Path $bundledDotnet).Path
|
||||
} else {
|
||||
(Get-Command dotnet -ErrorAction Stop).Source
|
||||
}
|
||||
$project = Join-Path $PSScriptRoot "src\SmartB2B.Enova.Service\SmartB2B.Enova.Service.csproj"
|
||||
$daemonSource = Join-Path $PSScriptRoot "daemon"
|
||||
$winSwSource = Resolve-Path (Join-Path $daemonSource "smartb2bsync-enova.exe")
|
||||
$output = [System.IO.Path]::GetFullPath($OutputPath)
|
||||
$daemonOutput = Join-Path $output "daemon"
|
||||
$existingConfigurationPath = Join-Path $output "config\enova.json"
|
||||
$existingConfiguration = if (Test-Path -LiteralPath $existingConfigurationPath) {
|
||||
[System.IO.File]::ReadAllText($existingConfigurationPath)
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($output -eq [System.IO.Path]::GetPathRoot($output) -or $output.Length -lt 10) {
|
||||
throw "Niebezpieczna ścieżka wyjściowa: $output"
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $output) {
|
||||
Remove-Item -LiteralPath $output -Recurse -Force
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $output | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $daemonOutput | Out-Null
|
||||
|
||||
& $dotnet publish $project -c $Configuration --no-restore -o $output
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Publikowanie usługi nie powiodło się."
|
||||
}
|
||||
|
||||
Copy-Item $winSwSource (Join-Path $daemonOutput "smartb2bsync-enova.exe") -Force
|
||||
Copy-Item (Join-Path $daemonSource "smartb2bsync-enova.xml") $daemonOutput -Force
|
||||
Copy-Item (Join-Path $daemonSource "install.cmd") $daemonOutput -Force
|
||||
Copy-Item (Join-Path $daemonSource "uninstall.cmd") $daemonOutput -Force
|
||||
|
||||
if ($null -ne $existingConfiguration) {
|
||||
[System.IO.File]::WriteAllText(
|
||||
(Join-Path $output "config\enova.json"),
|
||||
$existingConfiguration,
|
||||
[System.Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
|
||||
Write-Host "Gotowa usługa: $output"
|
||||
BIN
release/Newtonsoft.Json.dll
Normal file
BIN
release/Newtonsoft.Json.dll
Normal file
Binary file not shown.
BIN
release/NewtonsoftMessagePack.dll
Normal file
BIN
release/NewtonsoftMessagePack.dll
Normal file
Binary file not shown.
BIN
release/SmartB2B.Enova.Contracts.dll
Normal file
BIN
release/SmartB2B.Enova.Contracts.dll
Normal file
Binary file not shown.
BIN
release/SmartB2B.Enova.Contracts.pdb
Normal file
BIN
release/SmartB2B.Enova.Contracts.pdb
Normal file
Binary file not shown.
636
release/SmartB2B.Enova.Service.deps.json
Normal file
636
release/SmartB2B.Enova.Service.deps.json
Normal file
@@ -0,0 +1,636 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v8.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {
|
||||
"SmartB2B.Enova.Service/1.0.0": {
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"SmartB2B.Enova.Contracts": "1.0.0",
|
||||
"WampSharp.Default.Client": "23.8.1"
|
||||
},
|
||||
"runtime": {
|
||||
"SmartB2B.Enova.Service.dll": {}
|
||||
}
|
||||
},
|
||||
"Microsoft.CSharp/4.3.0": {
|
||||
"dependencies": {
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Diagnostics.Debug": "4.3.0",
|
||||
"System.Dynamic.Runtime": "4.3.0",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.Linq": "4.3.0",
|
||||
"System.Linq.Expressions": "4.3.0",
|
||||
"System.ObjectModel": "4.3.0",
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Reflection.Extensions": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Reflection.TypeExtensions": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Runtime.InteropServices": "4.3.0",
|
||||
"System.Threading": "4.3.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/1.1.0": {},
|
||||
"Microsoft.NETCore.Targets/1.1.0": {},
|
||||
"Newtonsoft.Json/13.0.3": {
|
||||
"runtime": {
|
||||
"lib/net6.0/Newtonsoft.Json.dll": {
|
||||
"assemblyVersion": "13.0.0.0",
|
||||
"fileVersion": "13.0.3.27908"
|
||||
}
|
||||
}
|
||||
},
|
||||
"NewtonsoftMessagePack/0.1.11": {
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "13.0.3"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/NewtonsoftMessagePack.dll": {
|
||||
"assemblyVersion": "0.0.0.0",
|
||||
"fileVersion": "0.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Collections/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Collections.Immutable/1.5.0": {},
|
||||
"System.Diagnostics.Debug/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Dynamic.Runtime/4.3.0": {
|
||||
"dependencies": {
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Diagnostics.Debug": "4.3.0",
|
||||
"System.Linq": "4.3.0",
|
||||
"System.Linq.Expressions": "4.3.0",
|
||||
"System.ObjectModel": "4.3.0",
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Reflection.Emit": "4.3.0",
|
||||
"System.Reflection.Emit.ILGeneration": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Reflection.TypeExtensions": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Threading": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Globalization/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.IO/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Linq/4.3.0": {
|
||||
"dependencies": {
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Diagnostics.Debug": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Linq.Expressions/4.3.0": {
|
||||
"dependencies": {
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Diagnostics.Debug": "4.3.0",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Linq": "4.3.0",
|
||||
"System.ObjectModel": "4.3.0",
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Reflection.Emit": "4.3.0",
|
||||
"System.Reflection.Emit.ILGeneration": "4.3.0",
|
||||
"System.Reflection.Emit.Lightweight": "4.3.0",
|
||||
"System.Reflection.Extensions": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Reflection.TypeExtensions": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Threading": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.ObjectModel/4.3.0": {
|
||||
"dependencies": {
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Diagnostics.Debug": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Threading": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reactive/4.1.6": {
|
||||
"dependencies": {
|
||||
"System.Runtime.InteropServices.WindowsRuntime": "4.3.0",
|
||||
"System.Threading.Tasks.Extensions": "4.5.2"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/System.Reactive.dll": {
|
||||
"assemblyVersion": "4.1.0.0",
|
||||
"fileVersion": "4.1.6.362"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Reflection/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection.Emit/4.3.0": {
|
||||
"dependencies": {
|
||||
"System.IO": "4.3.0",
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Reflection.Emit.ILGeneration": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection.Emit.ILGeneration/4.3.0": {
|
||||
"dependencies": {
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection.Emit.Lightweight/4.3.0": {
|
||||
"dependencies": {
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Reflection.Emit.ILGeneration": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection.Extensions/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection.Primitives/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection.TypeExtensions/4.3.0": {
|
||||
"dependencies": {
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Resources.ResourceManager/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.Extensions/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.Handles/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.InteropServices/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.InteropServices.WindowsRuntime/4.3.0": {
|
||||
"dependencies": {
|
||||
"System.Runtime": "4.3.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard1.3/System.Runtime.InteropServices.WindowsRuntime.dll": {
|
||||
"assemblyVersion": "4.0.2.0",
|
||||
"fileVersion": "4.6.24705.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Text.Encoding/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Threading/4.3.0": {
|
||||
"dependencies": {
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.Tasks/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.Tasks.Dataflow/4.9.0": {},
|
||||
"System.Threading.Tasks.Extensions/4.5.2": {},
|
||||
"WampSharp/23.8.1": {
|
||||
"dependencies": {
|
||||
"Microsoft.CSharp": "4.3.0",
|
||||
"System.Collections.Immutable": "1.5.0",
|
||||
"System.Reactive": "4.1.6",
|
||||
"System.Threading.Tasks.Dataflow": "4.9.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.1/WampSharp.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WampSharp.Default.Client/23.8.1": {
|
||||
"dependencies": {
|
||||
"WampSharp.NewtonsoftJson": "23.8.1",
|
||||
"WampSharp.NewtonsoftMessagePack": "23.8.1",
|
||||
"WampSharp.WebSockets": "23.8.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.1/WampSharp.Default.Client.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WampSharp.NewtonsoftJson/23.8.1": {
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"WampSharp": "23.8.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.1/WampSharp.NewtonsoftJson.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WampSharp.NewtonsoftMessagePack/23.8.1": {
|
||||
"dependencies": {
|
||||
"NewtonsoftMessagePack": "0.1.11",
|
||||
"WampSharp.NewtonsoftJson": "23.8.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.1/WampSharp.NewtonsoftMessagePack.dll": {
|
||||
"assemblyVersion": "0.0.0.0",
|
||||
"fileVersion": "0.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WampSharp.WebSockets/23.8.1": {
|
||||
"dependencies": {
|
||||
"WampSharp": "23.8.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.1/WampSharp.WebSockets.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SmartB2B.Enova.Contracts/1.0.0": {
|
||||
"runtime": {
|
||||
"SmartB2B.Enova.Contracts.dll": {
|
||||
"assemblyVersion": "1.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"SmartB2B.Enova.Service/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Microsoft.CSharp/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-P+MBhIM0YX+JqROuf7i306ZLJEjQYA9uUyRDE+OqwUI5sh41e2ZbPQV3LfAPh+29cmceE1pUffXsGfR4eMY3KA==",
|
||||
"path": "microsoft.csharp/4.3.0",
|
||||
"hashPath": "microsoft.csharp.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/1.1.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
|
||||
"path": "microsoft.netcore.platforms/1.1.0",
|
||||
"hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.NETCore.Targets/1.1.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==",
|
||||
"path": "microsoft.netcore.targets/1.1.0",
|
||||
"hashPath": "microsoft.netcore.targets.1.1.0.nupkg.sha512"
|
||||
},
|
||||
"Newtonsoft.Json/13.0.3": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
|
||||
"path": "newtonsoft.json/13.0.3",
|
||||
"hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
|
||||
},
|
||||
"NewtonsoftMessagePack/0.1.11": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-CuwKoxWUFRQewUXS0FH2V3hZFrati69OtveyX3GwlDGp6VuvkzaSzSXw7Hi5OiFR3X4QYULlWkwV/jxYYEPacA==",
|
||||
"path": "newtonsoftmessagepack/0.1.11",
|
||||
"hashPath": "newtonsoftmessagepack.0.1.11.nupkg.sha512"
|
||||
},
|
||||
"System.Collections/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==",
|
||||
"path": "system.collections/4.3.0",
|
||||
"hashPath": "system.collections.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Collections.Immutable/1.5.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-EXKiDFsChZW0RjrZ4FYHu9aW6+P4MCgEDCklsVseRfhoO0F+dXeMSsMRAlVXIo06kGJ/zv+2w1a2uc2+kxxSaQ==",
|
||||
"path": "system.collections.immutable/1.5.0",
|
||||
"hashPath": "system.collections.immutable.1.5.0.nupkg.sha512"
|
||||
},
|
||||
"System.Diagnostics.Debug/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==",
|
||||
"path": "system.diagnostics.debug/4.3.0",
|
||||
"hashPath": "system.diagnostics.debug.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Dynamic.Runtime/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==",
|
||||
"path": "system.dynamic.runtime/4.3.0",
|
||||
"hashPath": "system.dynamic.runtime.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Globalization/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==",
|
||||
"path": "system.globalization/4.3.0",
|
||||
"hashPath": "system.globalization.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.IO/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
|
||||
"path": "system.io/4.3.0",
|
||||
"hashPath": "system.io.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Linq/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==",
|
||||
"path": "system.linq/4.3.0",
|
||||
"hashPath": "system.linq.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Linq.Expressions/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==",
|
||||
"path": "system.linq.expressions/4.3.0",
|
||||
"hashPath": "system.linq.expressions.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.ObjectModel/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==",
|
||||
"path": "system.objectmodel/4.3.0",
|
||||
"hashPath": "system.objectmodel.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Reactive/4.1.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-mPLRPf2tql0Af4RcJRVPWiGxhJCbPQFNr74Qq0rDf+QXt8KV/SzYl9/NK604zf0s8Yu3hwelmCsDCjqp5SJoJA==",
|
||||
"path": "system.reactive/4.1.6",
|
||||
"hashPath": "system.reactive.4.1.6.nupkg.sha512"
|
||||
},
|
||||
"System.Reflection/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
|
||||
"path": "system.reflection/4.3.0",
|
||||
"hashPath": "system.reflection.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Reflection.Emit/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==",
|
||||
"path": "system.reflection.emit/4.3.0",
|
||||
"hashPath": "system.reflection.emit.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Reflection.Emit.ILGeneration/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==",
|
||||
"path": "system.reflection.emit.ilgeneration/4.3.0",
|
||||
"hashPath": "system.reflection.emit.ilgeneration.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Reflection.Emit.Lightweight/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==",
|
||||
"path": "system.reflection.emit.lightweight/4.3.0",
|
||||
"hashPath": "system.reflection.emit.lightweight.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Reflection.Extensions/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==",
|
||||
"path": "system.reflection.extensions/4.3.0",
|
||||
"hashPath": "system.reflection.extensions.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Reflection.Primitives/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
|
||||
"path": "system.reflection.primitives/4.3.0",
|
||||
"hashPath": "system.reflection.primitives.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Reflection.TypeExtensions/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==",
|
||||
"path": "system.reflection.typeextensions/4.3.0",
|
||||
"hashPath": "system.reflection.typeextensions.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Resources.ResourceManager/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==",
|
||||
"path": "system.resources.resourcemanager/4.3.0",
|
||||
"hashPath": "system.resources.resourcemanager.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Runtime/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
||||
"path": "system.runtime/4.3.0",
|
||||
"hashPath": "system.runtime.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Runtime.Extensions/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==",
|
||||
"path": "system.runtime.extensions/4.3.0",
|
||||
"hashPath": "system.runtime.extensions.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Runtime.Handles/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==",
|
||||
"path": "system.runtime.handles/4.3.0",
|
||||
"hashPath": "system.runtime.handles.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Runtime.InteropServices/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==",
|
||||
"path": "system.runtime.interopservices/4.3.0",
|
||||
"hashPath": "system.runtime.interopservices.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Runtime.InteropServices.WindowsRuntime/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-J4GUi3xZQLUBasNwZnjrffN8i5wpHrBtZoLG+OhRyGo/+YunMRWWtwoMDlUAIdmX0uRfpHIBDSV6zyr3yf00TA==",
|
||||
"path": "system.runtime.interopservices.windowsruntime/4.3.0",
|
||||
"hashPath": "system.runtime.interopservices.windowsruntime.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Text.Encoding/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
|
||||
"path": "system.text.encoding/4.3.0",
|
||||
"hashPath": "system.text.encoding.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Threading/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==",
|
||||
"path": "system.threading/4.3.0",
|
||||
"hashPath": "system.threading.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Threading.Tasks/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
|
||||
"path": "system.threading.tasks/4.3.0",
|
||||
"hashPath": "system.threading.tasks.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Threading.Tasks.Dataflow/4.9.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-dTS+3D/GtG2/Pvc3E5YzVvAa7aQJgLDlZDIzukMOJjYudVOQOUXEU68y6Zi3Nn/jqIeB5kOCwrGbQFAKHVzXEQ==",
|
||||
"path": "system.threading.tasks.dataflow/4.9.0",
|
||||
"hashPath": "system.threading.tasks.dataflow.4.9.0.nupkg.sha512"
|
||||
},
|
||||
"System.Threading.Tasks.Extensions/4.5.2": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-BG/TNxDFv0svAzx8OiMXDlsHfGw623BZ8tCXw4YLhDFDvDhNUEV58jKYMGRnkbJNm7c3JNNJDiN7JBMzxRBR2w==",
|
||||
"path": "system.threading.tasks.extensions/4.5.2",
|
||||
"hashPath": "system.threading.tasks.extensions.4.5.2.nupkg.sha512"
|
||||
},
|
||||
"WampSharp/23.8.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-khUXgLMNv5KQ9Xzq5HkxGP9kb/5r19UMS7RW8kY+5ua3ev6ZtX1zGUaMUHwTiXVWkb+1wz4B35oHqx/F38sicQ==",
|
||||
"path": "wampsharp/23.8.1",
|
||||
"hashPath": "wampsharp.23.8.1.nupkg.sha512"
|
||||
},
|
||||
"WampSharp.Default.Client/23.8.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-RZD8/SOQb/6KOhpxk2Z5IyVh06AJU/v431En2t14VCLD++RU7nSE4fJzNbQTlirbq/tYEVmTiYHILWX9HIEGUg==",
|
||||
"path": "wampsharp.default.client/23.8.1",
|
||||
"hashPath": "wampsharp.default.client.23.8.1.nupkg.sha512"
|
||||
},
|
||||
"WampSharp.NewtonsoftJson/23.8.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-4JMyyf5ojLivoZusJ0eTL73p2tDrKR4pS8fy7iiZ4IxNV3D+ZzuVQc8uh6y9qtwXEx9IImrjvRejbphiBw3ERA==",
|
||||
"path": "wampsharp.newtonsoftjson/23.8.1",
|
||||
"hashPath": "wampsharp.newtonsoftjson.23.8.1.nupkg.sha512"
|
||||
},
|
||||
"WampSharp.NewtonsoftMessagePack/23.8.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-wD/7AD6ZHyQOMcHuCn69pm4PMRHJL6LAvqAXZuL6/yxP4ASk8qaDf088sL1gjWmxiHSAHFnBsXDSZJzRncDfUA==",
|
||||
"path": "wampsharp.newtonsoftmessagepack/23.8.1",
|
||||
"hashPath": "wampsharp.newtonsoftmessagepack.23.8.1.nupkg.sha512"
|
||||
},
|
||||
"WampSharp.WebSockets/23.8.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-sPdCJhkNS7AzQB62/DeG2EjQGud2VmJ5Hd4l1OOn2NmEqHx5bTyUcUUXaiRc4F4JtgRwiBU1Le3kd5SjpUfZsg==",
|
||||
"path": "wampsharp.websockets/23.8.1",
|
||||
"hashPath": "wampsharp.websockets.23.8.1.nupkg.sha512"
|
||||
},
|
||||
"SmartB2B.Enova.Contracts/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
release/SmartB2B.Enova.Service.dll
Normal file
BIN
release/SmartB2B.Enova.Service.dll
Normal file
Binary file not shown.
BIN
release/SmartB2B.Enova.Service.exe
Normal file
BIN
release/SmartB2B.Enova.Service.exe
Normal file
Binary file not shown.
BIN
release/SmartB2B.Enova.Service.pdb
Normal file
BIN
release/SmartB2B.Enova.Service.pdb
Normal file
Binary file not shown.
13
release/SmartB2B.Enova.Service.runtimeconfig.json
Normal file
13
release/SmartB2B.Enova.Service.runtimeconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"framework": {
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
"configProperties": {
|
||||
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
release/System.Reactive.dll
Normal file
BIN
release/System.Reactive.dll
Normal file
Binary file not shown.
BIN
release/System.Runtime.InteropServices.WindowsRuntime.dll
Normal file
BIN
release/System.Runtime.InteropServices.WindowsRuntime.dll
Normal file
Binary file not shown.
BIN
release/WampSharp.Default.Client.dll
Normal file
BIN
release/WampSharp.Default.Client.dll
Normal file
Binary file not shown.
BIN
release/WampSharp.NewtonsoftJson.dll
Normal file
BIN
release/WampSharp.NewtonsoftJson.dll
Normal file
Binary file not shown.
BIN
release/WampSharp.NewtonsoftMessagePack.dll
Normal file
BIN
release/WampSharp.NewtonsoftMessagePack.dll
Normal file
Binary file not shown.
BIN
release/WampSharp.WebSockets.dll
Normal file
BIN
release/WampSharp.WebSockets.dll
Normal file
Binary file not shown.
BIN
release/WampSharp.dll
Normal file
BIN
release/WampSharp.dll
Normal file
Binary file not shown.
BIN
release/adapters/SmartB2B.Enova.Adapter.dll
Normal file
BIN
release/adapters/SmartB2B.Enova.Adapter.dll
Normal file
Binary file not shown.
24
release/config/enova.json
Normal file
24
release/config/enova.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"portal": "demo",
|
||||
"wamp": {
|
||||
"serverUrl": "ws://wamp.smartb2b.eu:8080/",
|
||||
"reconnectDelaySeconds": 10
|
||||
},
|
||||
"enova": {
|
||||
"installationPath": "C:\\Program Files (x86)\\Soneta\\enova365 2604.4.4",
|
||||
"database": "Firma demo",
|
||||
"operator": "Administrator",
|
||||
"passwordEnvironmentVariable": "SMARTB2B_ENOVA_PASSWORD",
|
||||
"documentDefinition": "ZO",
|
||||
"defaultWarehouseCode": null,
|
||||
"saveMode": "Buffer"
|
||||
},
|
||||
"sql": {
|
||||
"connectionStringEnvironmentVariable": "SMARTB2B_SQL_CONNECTION_STRING",
|
||||
"commandTimeoutSeconds": 30
|
||||
},
|
||||
"diagnostics": {
|
||||
"logDirectory": "daemon",
|
||||
"logPrefix": "smartb2bsync-enova"
|
||||
}
|
||||
}
|
||||
14
release/daemon/install.cmd
Normal file
14
release/daemon/install.cmd
Normal file
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
if not exist "smartb2bsync-enova.exe" (
|
||||
echo Brak smartb2bsync-enova.exe. Najpierw uruchom publish.ps1.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
smartb2bsync-enova.exe install
|
||||
if errorlevel 1 exit /b %errorlevel%
|
||||
|
||||
smartb2bsync-enova.exe start
|
||||
exit /b %errorlevel%
|
||||
BIN
release/daemon/smartb2bsync-enova.exe
Normal file
BIN
release/daemon/smartb2bsync-enova.exe
Normal file
Binary file not shown.
13
release/daemon/smartb2bsync-enova.xml
Normal file
13
release/daemon/smartb2bsync-enova.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<service>
|
||||
<id>smartb2bsync-enova</id>
|
||||
<name>SmartB2B SYNC - enova365</name>
|
||||
<description>Synchronizuje enova365 z portalem SmartB2B.</description>
|
||||
<executable>%BASE%\..\SmartB2B.Enova.Service.exe</executable>
|
||||
<workingdirectory>%BASE%\..</workingdirectory>
|
||||
<stoptimeout>30sec</stoptimeout>
|
||||
<log mode="roll-by-time">
|
||||
<pattern>yyyyMMdd</pattern>
|
||||
</log>
|
||||
<onfailure action="restart" delay="10 sec" />
|
||||
<delayedAutoStart />
|
||||
</service>
|
||||
7
release/daemon/uninstall.cmd
Normal file
7
release/daemon/uninstall.cmd
Normal file
@@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
smartb2bsync-enova.exe stop
|
||||
smartb2bsync-enova.exe uninstall
|
||||
exit /b %errorlevel%
|
||||
256
src/SmartB2B.Enova.Adapter/EnovaOrderAdapter.cs
Normal file
256
src/SmartB2B.Enova.Adapter/EnovaOrderAdapter.cs
Normal file
@@ -0,0 +1,256 @@
|
||||
using System.Reflection;
|
||||
using SmartB2B.Enova.Contracts;
|
||||
using Soneta.Business;
|
||||
using Soneta.Business.App;
|
||||
using Soneta.CRM;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Magazyny;
|
||||
using Soneta.Start;
|
||||
using Soneta.Tools;
|
||||
using Soneta.Towary;
|
||||
using Soneta.Types;
|
||||
|
||||
namespace SmartB2B.Enova.Adapter;
|
||||
|
||||
public sealed class EnovaOrderAdapter : IEnovaOrderAdapter
|
||||
{
|
||||
private const string CurrencySymbol = "PLN";
|
||||
private static readonly object RuntimeLock = new();
|
||||
private static bool _runtimeLoaded;
|
||||
private EnovaAdapterConfiguration? _configuration;
|
||||
|
||||
public EnovaAdapterInfo Initialize(EnovaAdapterConfiguration configuration)
|
||||
{
|
||||
ValidateConfiguration(configuration);
|
||||
EnsureRuntimeLoaded();
|
||||
|
||||
_ = BusApplication.Instance[configuration.Connection.DatabaseName]
|
||||
?? throw new EnovaOperationException(
|
||||
"eu.smartb2b.erp.database_not_found",
|
||||
$"Nie znaleziono zarejestrowanej bazy enova365 '{configuration.Connection.DatabaseName}'.",
|
||||
new Dictionary<string, object?> { ["database"] = configuration.Connection.DatabaseName });
|
||||
|
||||
_configuration = configuration;
|
||||
|
||||
return new EnovaAdapterInfo(
|
||||
GetAssemblyVersion(typeof(EnovaOrderAdapter).Assembly),
|
||||
GetAssemblyVersion(typeof(Session).Assembly),
|
||||
GetAssemblyVersion(typeof(HandelModule).Assembly),
|
||||
configuration.Connection.DatabaseName);
|
||||
}
|
||||
|
||||
public OrderResult CreateOrder(OrderRequest request)
|
||||
{
|
||||
var configuration = _configuration
|
||||
?? throw new InvalidOperationException("Adapter Enovy nie został zainicjalizowany.");
|
||||
|
||||
var errors = OrderValidator.Validate(request);
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
throw new EnovaOperationException(
|
||||
"eu.smartb2b.erp.invalid_order_lines",
|
||||
string.Join(" | ", errors),
|
||||
new Dictionary<string, object?> { ["errors"] = errors });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var sessionState = SessionState.Create();
|
||||
using var attachedSessionState = sessionState.Attach();
|
||||
|
||||
var database = BusApplication.Instance[configuration.Connection.DatabaseName]
|
||||
?? throw new EnovaOperationException(
|
||||
"eu.smartb2b.erp.database_not_found",
|
||||
$"Nie znaleziono zarejestrowanej bazy enova365 '{configuration.Connection.DatabaseName}'.");
|
||||
|
||||
using var login = database.Login(
|
||||
winAuth: false,
|
||||
user: configuration.Connection.OperatorName,
|
||||
password: configuration.Connection.Password);
|
||||
using var session = login.CreateSession(
|
||||
readOnly: false,
|
||||
config: false,
|
||||
name: "SmartB2B Enova Sync");
|
||||
|
||||
return CreateOrderInSession(session, request);
|
||||
}
|
||||
catch (EnovaOperationException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new EnovaOperationException(
|
||||
"eu.smartb2b.erp.error",
|
||||
$"Nie udało się utworzyć zamówienia w bazie '{configuration.Connection.DatabaseName}': {exception.Message}",
|
||||
new Dictionary<string, object?> { ["message"] = exception.Message },
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static OrderResult CreateOrderInSession(Session session, OrderRequest request)
|
||||
{
|
||||
var handel = HandelModule.GetInstance(session);
|
||||
var crm = CRMModule.GetInstance(session);
|
||||
var towary = TowaryModule.GetInstance(session);
|
||||
var warehouses = MagazynyModule.GetInstance(session).Magazyny;
|
||||
|
||||
var definition = handel.DefDokHandlowych.WgSymbolu[request.DocumentDefinition]
|
||||
?? throw new EnovaOperationException(
|
||||
"eu.smartb2b.erp.document_definition_not_found",
|
||||
$"Nie znaleziono definicji dokumentu '{request.DocumentDefinition}'.",
|
||||
new Dictionary<string, object?> { ["documentDefinition"] = request.DocumentDefinition });
|
||||
var customer = crm.Kontrahenci.WgKodu[request.CustomerCode]
|
||||
?? throw new EnovaOperationException(
|
||||
"eu.smartb2b.company_not_found",
|
||||
$"Nie znaleziono kontrahenta o kodzie '{request.CustomerCode}'.",
|
||||
new Dictionary<string, object?> { ["companyErpId"] = request.CustomerCode });
|
||||
var warehouse = string.IsNullOrWhiteSpace(request.WarehouseCode)
|
||||
? warehouses.StandardowyMagazyn
|
||||
: warehouses.GetGrantedView()
|
||||
.Cast<Magazyn>()
|
||||
.FirstOrDefault(candidate => string.Equals(
|
||||
candidate.Symbol,
|
||||
request.WarehouseCode.Trim(),
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (warehouse is null)
|
||||
{
|
||||
throw new EnovaOperationException(
|
||||
"eu.smartb2b.erp.warehouse_not_found",
|
||||
string.IsNullOrWhiteSpace(request.WarehouseCode)
|
||||
? "Nie znaleziono standardowego magazynu firmy."
|
||||
: $"Nie znaleziono magazynu o symbolu '{request.WarehouseCode.Trim()}'.",
|
||||
new Dictionary<string, object?> { ["warehouseErpId"] = request.WarehouseCode });
|
||||
}
|
||||
|
||||
DokumentHandlowy document;
|
||||
|
||||
using (var transaction = session.Logout(editMode: true))
|
||||
{
|
||||
document = session.AddRow(new DokumentHandlowy());
|
||||
document.Definicja = definition;
|
||||
document.Kontrahent = customer;
|
||||
document.Magazyn = warehouse;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.CustomerReference))
|
||||
{
|
||||
document.Obcy.Numer = request.CustomerReference.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Notes))
|
||||
{
|
||||
document.Opis.Clear();
|
||||
document.Opis.Add(request.Notes.Trim());
|
||||
}
|
||||
|
||||
foreach (var line in request.Items)
|
||||
{
|
||||
var product = towary.Towary.WgKodu[line.ProductCode]
|
||||
?? throw new EnovaOperationException(
|
||||
"eu.smartb2b.product_not_found",
|
||||
$"Nie znaleziono towaru o kodzie '{line.ProductCode}'.",
|
||||
new Dictionary<string, object?> { ["symbol"] = line.ProductCode });
|
||||
|
||||
var position = session.AddRow(new PozycjaDokHandlowego(document));
|
||||
position.Towar = product;
|
||||
position.Ilosc = new Quantity((double)line.Quantity);
|
||||
position.Cena = new DoubleCy(line.UnitPrice, CurrencySymbol);
|
||||
position.UstawRabat(new Percent(line.Discount / 100m), ręcznie: true);
|
||||
}
|
||||
|
||||
document.Stan = request.SaveMode switch
|
||||
{
|
||||
OrderSaveMode.Buffer => StanDokumentuHandlowego.Bufor,
|
||||
OrderSaveMode.Approved => StanDokumentuHandlowego.Zatwierdzony,
|
||||
_ => throw new EnovaOperationException(
|
||||
"eu.smartb2b.erp.invalid_order",
|
||||
$"Nieobsługiwany tryb zapisu: {request.SaveMode}.")
|
||||
};
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
session.Save();
|
||||
|
||||
return new OrderResult(
|
||||
document.ID,
|
||||
document.Numer.Pelny.ToString(),
|
||||
request.SaveMode,
|
||||
customer.Kod,
|
||||
warehouse.Symbol,
|
||||
document.Obcy.Numer,
|
||||
request.Items.Count,
|
||||
document.Suma.Netto,
|
||||
document.Suma.VAT,
|
||||
document.Suma.Brutto,
|
||||
request.Items.Any(item => item.Discount > 0));
|
||||
}
|
||||
|
||||
private static void EnsureRuntimeLoaded()
|
||||
{
|
||||
if (_runtimeLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (RuntimeLock)
|
||||
{
|
||||
if (_runtimeLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var loader = new Loader
|
||||
{
|
||||
WithUI = false,
|
||||
WithNet = false,
|
||||
WithExtra = false,
|
||||
WithExtensions = false,
|
||||
CheckWinForms = false
|
||||
};
|
||||
|
||||
loader.UseCurrentDllsAsInstallation(standard: true);
|
||||
loader.Load();
|
||||
|
||||
var errors = Loader.GetListOfErrors();
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
throw new EnovaOperationException(
|
||||
"eu.smartb2b.erp.runtime_error",
|
||||
$"Loader enova365 zgłosił błędy: {string.Join(" | ", errors)}");
|
||||
}
|
||||
|
||||
_runtimeLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateConfiguration(EnovaAdapterConfiguration configuration)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(configuration.Connection.DatabaseName))
|
||||
{
|
||||
errors.Add("Nazwa bazy enova365 jest wymagana.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(configuration.Connection.OperatorName))
|
||||
{
|
||||
errors.Add("Nazwa operatora enova365 jest wymagana.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(configuration.DocumentDefinition))
|
||||
{
|
||||
errors.Add("Definicja dokumentu jest wymagana.");
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
throw new EnovaOperationException(
|
||||
"eu.smartb2b.erp.configuration_error",
|
||||
string.Join(" | ", errors));
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetAssemblyVersion(Assembly assembly) =>
|
||||
assembly.GetName().Version?.ToString() ?? "unknown";
|
||||
}
|
||||
17
src/SmartB2B.Enova.Adapter/SmartB2B.Enova.Adapter.csproj
Normal file
17
src/SmartB2B.Enova.Adapter/SmartB2B.Enova.Adapter.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../SmartB2B.Enova.Contracts/SmartB2B.Enova.Contracts.csproj" />
|
||||
<PackageReference Include="Soneta.Handel" Version="2604.4.4" />
|
||||
<PackageReference Include="Soneta.Business.Licence" Version="2604.4.4" />
|
||||
<PackageReference Include="Soneta.Licence.Local.Proxy" Version="1.0.2" />
|
||||
<PackageReference Include="Soneta.Products.Modules" Version="2604.4.4" />
|
||||
<PackageReference Include="Soneta.Start" Version="2604.4.4" />
|
||||
<PackageReference Include="Soneta.Standard" Version="2604.4.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
154
src/SmartB2B.Enova.Contracts/EnovaContracts.cs
Normal file
154
src/SmartB2B.Enova.Contracts/EnovaContracts.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
namespace SmartB2B.Enova.Contracts;
|
||||
|
||||
public enum OrderSaveMode
|
||||
{
|
||||
Buffer,
|
||||
Approved
|
||||
}
|
||||
|
||||
public sealed record EnovaConnectionOptions(
|
||||
string DatabaseName,
|
||||
string OperatorName,
|
||||
string Password);
|
||||
|
||||
public sealed record EnovaAdapterConfiguration(
|
||||
EnovaConnectionOptions Connection,
|
||||
string DocumentDefinition,
|
||||
string? DefaultWarehouseCode,
|
||||
OrderSaveMode SaveMode);
|
||||
|
||||
public sealed class OrderRequest
|
||||
{
|
||||
public string CustomerCode { get; init; } = string.Empty;
|
||||
|
||||
public string? CustomerReference { get; init; }
|
||||
|
||||
public string? Notes { get; init; }
|
||||
|
||||
public string DocumentDefinition { get; init; } = "ZO";
|
||||
|
||||
public string? WarehouseCode { get; init; }
|
||||
|
||||
public OrderSaveMode SaveMode { get; init; } = OrderSaveMode.Buffer;
|
||||
|
||||
public IReadOnlyList<OrderLineRequest> Items { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class OrderLineRequest
|
||||
{
|
||||
public string ProductCode { get; init; } = string.Empty;
|
||||
|
||||
public decimal Quantity { get; init; }
|
||||
|
||||
public decimal UnitPrice { get; init; }
|
||||
|
||||
public decimal Discount { get; init; }
|
||||
}
|
||||
|
||||
public sealed record OrderResult(
|
||||
int Id,
|
||||
string Number,
|
||||
OrderSaveMode SaveMode,
|
||||
string CustomerCode,
|
||||
string WarehouseCode,
|
||||
string? CustomerReference,
|
||||
int LineCount,
|
||||
decimal Net,
|
||||
decimal Vat,
|
||||
decimal Gross,
|
||||
bool HasDiscount);
|
||||
|
||||
public sealed record EnovaAdapterInfo(
|
||||
string AdapterVersion,
|
||||
string SonetaBusinessVersion,
|
||||
string SonetaHandelVersion,
|
||||
string DatabaseName);
|
||||
|
||||
public interface IEnovaOrderAdapter
|
||||
{
|
||||
EnovaAdapterInfo Initialize(EnovaAdapterConfiguration configuration);
|
||||
|
||||
OrderResult CreateOrder(OrderRequest request);
|
||||
}
|
||||
|
||||
public sealed class EnovaOperationException : Exception
|
||||
{
|
||||
public EnovaOperationException(
|
||||
string errorUri,
|
||||
string message,
|
||||
IReadOnlyDictionary<string, object?>? details = null,
|
||||
Exception? innerException = null)
|
||||
: base(message, innerException)
|
||||
{
|
||||
ErrorUri = errorUri;
|
||||
Details = details ?? new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
public string ErrorUri { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, object?> Details { get; }
|
||||
}
|
||||
|
||||
public static class OrderValidator
|
||||
{
|
||||
public static IReadOnlyList<string> Validate(OrderRequest? request)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (request is null)
|
||||
{
|
||||
errors.Add("Brak danych zamówienia.");
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.CustomerCode))
|
||||
{
|
||||
errors.Add("Pole companyErpId jest wymagane.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.DocumentDefinition))
|
||||
{
|
||||
errors.Add("Definicja dokumentu jest wymagana.");
|
||||
}
|
||||
|
||||
if (request.Items is null || request.Items.Count == 0)
|
||||
{
|
||||
errors.Add("Zamówienie musi zawierać co najmniej jedną pozycję.");
|
||||
return errors;
|
||||
}
|
||||
|
||||
for (var index = 0; index < request.Items.Count; index++)
|
||||
{
|
||||
var line = request.Items[index];
|
||||
var prefix = $"lines[{index}]";
|
||||
|
||||
if (line is null)
|
||||
{
|
||||
errors.Add($"{prefix}: pozycja nie może być pusta.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line.ProductCode))
|
||||
{
|
||||
errors.Add($"{prefix}.symbol jest wymagane.");
|
||||
}
|
||||
|
||||
if (line.Quantity <= 0)
|
||||
{
|
||||
errors.Add($"{prefix}.quantity musi być większe od zera.");
|
||||
}
|
||||
|
||||
if (line.UnitPrice < 0)
|
||||
{
|
||||
errors.Add($"{prefix}.price_netto nie może być ujemne.");
|
||||
}
|
||||
|
||||
if (line.Discount is < 0 or > 100)
|
||||
{
|
||||
errors.Add($"{prefix}.discount musi mieścić się w zakresie 0-100.");
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
171
src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs
Normal file
171
src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using SmartB2B.Enova.Contracts;
|
||||
|
||||
namespace SmartB2B.Enova.Service.Configuration;
|
||||
|
||||
public sealed class ServiceSettings
|
||||
{
|
||||
public string Portal { get; init; } = string.Empty;
|
||||
|
||||
public WampSettings Wamp { get; init; } = new();
|
||||
|
||||
public EnovaSettings Enova { get; init; } = new();
|
||||
|
||||
public SqlSettings Sql { get; init; } = new();
|
||||
|
||||
public DiagnosticsSettings Diagnostics { get; init; } = new();
|
||||
|
||||
public static ServiceSettings Load(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new ConfigurationException($"Nie znaleziono pliku konfiguracji: {path}");
|
||||
}
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
ServiceSettings settings;
|
||||
try
|
||||
{
|
||||
settings = JsonSerializer.Deserialize<ServiceSettings>(File.ReadAllText(path), options)
|
||||
?? throw new ConfigurationException("Plik konfiguracji jest pusty.");
|
||||
}
|
||||
catch (JsonException exception)
|
||||
{
|
||||
throw new ConfigurationException($"Niepoprawny JSON konfiguracji: {exception.Message}", exception);
|
||||
}
|
||||
|
||||
settings.Validate();
|
||||
return settings;
|
||||
}
|
||||
|
||||
public RuntimeSettings ResolveRuntimeSettings()
|
||||
{
|
||||
var enovaPassword = Environment.GetEnvironmentVariable(Enova.PasswordEnvironmentVariable);
|
||||
if (enovaPassword is null)
|
||||
{
|
||||
throw new ConfigurationException(
|
||||
$"Brak zmiennej środowiskowej {Enova.PasswordEnvironmentVariable} z hasłem operatora Enovy.");
|
||||
}
|
||||
|
||||
var connectionString = Environment.GetEnvironmentVariable(Sql.ConnectionStringEnvironmentVariable);
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
throw new ConfigurationException(
|
||||
$"Brak zmiennej środowiskowej {Sql.ConnectionStringEnvironmentVariable} z connection stringiem SQL.");
|
||||
}
|
||||
|
||||
var installationPath = Path.GetFullPath(
|
||||
Environment.ExpandEnvironmentVariables(Enova.InstallationPath));
|
||||
|
||||
return new RuntimeSettings(
|
||||
installationPath,
|
||||
new EnovaAdapterConfiguration(
|
||||
new EnovaConnectionOptions(Enova.Database, Enova.Operator, enovaPassword),
|
||||
Enova.DocumentDefinition,
|
||||
Enova.DefaultWarehouseCode,
|
||||
Enova.SaveMode),
|
||||
connectionString,
|
||||
Sql.CommandTimeoutSeconds,
|
||||
ResolvePath(Diagnostics.LogDirectory),
|
||||
Diagnostics.LogPrefix);
|
||||
}
|
||||
|
||||
private void Validate()
|
||||
{
|
||||
var errors = new List<string>();
|
||||
Require(Portal, "portal", errors);
|
||||
Require(Wamp.ServerUrl, "wamp.serverUrl", errors);
|
||||
if (Wamp.ReconnectDelaySeconds <= 0)
|
||||
{
|
||||
errors.Add("wamp.reconnectDelaySeconds musi być większe od zera.");
|
||||
}
|
||||
Require(Enova.InstallationPath, "enova.installationPath", errors);
|
||||
Require(Enova.Database, "enova.database", errors);
|
||||
Require(Enova.Operator, "enova.operator", errors);
|
||||
Require(Enova.PasswordEnvironmentVariable, "enova.passwordEnvironmentVariable", errors);
|
||||
Require(Enova.DocumentDefinition, "enova.documentDefinition", errors);
|
||||
Require(Sql.ConnectionStringEnvironmentVariable, "sql.connectionStringEnvironmentVariable", errors);
|
||||
if (Sql.CommandTimeoutSeconds <= 0)
|
||||
{
|
||||
errors.Add("sql.commandTimeoutSeconds musi być większe od zera.");
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
throw new ConfigurationException(string.Join(Environment.NewLine, errors));
|
||||
}
|
||||
}
|
||||
|
||||
private static void Require(string? value, string name, ICollection<string> errors)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
errors.Add($"Parametr '{name}' jest wymagany.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolvePath(string path) =>
|
||||
Path.IsPathRooted(path) ? Path.GetFullPath(path) : Path.GetFullPath(path, AppContext.BaseDirectory);
|
||||
}
|
||||
|
||||
public sealed class WampSettings
|
||||
{
|
||||
public string ServerUrl { get; init; } = string.Empty;
|
||||
|
||||
public int ReconnectDelaySeconds { get; init; } = 10;
|
||||
}
|
||||
|
||||
public sealed class EnovaSettings
|
||||
{
|
||||
public string InstallationPath { get; init; } = string.Empty;
|
||||
|
||||
public string Database { get; init; } = string.Empty;
|
||||
|
||||
public string Operator { get; init; } = "Administrator";
|
||||
|
||||
public string PasswordEnvironmentVariable { get; init; } = "SMARTB2B_ENOVA_PASSWORD";
|
||||
|
||||
public string DocumentDefinition { get; init; } = "ZO";
|
||||
|
||||
public string? DefaultWarehouseCode { get; init; }
|
||||
|
||||
public OrderSaveMode SaveMode { get; init; } = OrderSaveMode.Buffer;
|
||||
}
|
||||
|
||||
public sealed class SqlSettings
|
||||
{
|
||||
public string ConnectionStringEnvironmentVariable { get; init; } = "SMARTB2B_SQL_CONNECTION_STRING";
|
||||
|
||||
public int CommandTimeoutSeconds { get; init; } = 30;
|
||||
}
|
||||
|
||||
public sealed class DiagnosticsSettings
|
||||
{
|
||||
public string LogDirectory { get; init; } = "daemon";
|
||||
|
||||
public string LogPrefix { get; init; } = "smartb2bsync-enova";
|
||||
}
|
||||
|
||||
public sealed record RuntimeSettings(
|
||||
string EnovaInstallationPath,
|
||||
EnovaAdapterConfiguration EnovaConfiguration,
|
||||
string SqlConnectionString,
|
||||
int SqlCommandTimeoutSeconds,
|
||||
string LogDirectory,
|
||||
string LogPrefix);
|
||||
|
||||
public sealed class ConfigurationException : Exception
|
||||
{
|
||||
public ConfigurationException(string message, Exception? innerException = null)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
83
src/SmartB2B.Enova.Service/Diagnostics/DiagnosticsHandler.cs
Normal file
83
src/SmartB2B.Enova.Service/Diagnostics/DiagnosticsHandler.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using SmartB2B.Enova.Contracts;
|
||||
using SmartB2B.Enova.Service.Rpc;
|
||||
|
||||
namespace SmartB2B.Enova.Service.Diagnostics;
|
||||
|
||||
public sealed partial class DiagnosticsHandler
|
||||
{
|
||||
private readonly EnovaAdapterInfo _adapterInfo;
|
||||
private readonly string _enovaPath;
|
||||
private readonly string _logDirectory;
|
||||
private readonly string _logPrefix;
|
||||
|
||||
public DiagnosticsHandler(
|
||||
EnovaAdapterInfo adapterInfo,
|
||||
string enovaPath,
|
||||
string logDirectory,
|
||||
string logPrefix)
|
||||
{
|
||||
_adapterInfo = adapterInfo;
|
||||
_enovaPath = enovaPath;
|
||||
_logDirectory = logDirectory;
|
||||
_logPrefix = logPrefix;
|
||||
}
|
||||
|
||||
public Task<object?> GetVersionAsync(RpcInvocation _, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "unknown";
|
||||
return Task.FromResult<object?>(version);
|
||||
}
|
||||
|
||||
public Task<object?> GetInfoAsync(RpcInvocation _, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var serviceVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "unknown";
|
||||
var lines = new[]
|
||||
{
|
||||
$"SmartB2B Enova Sync version: {serviceVersion}",
|
||||
$".NET version: {Environment.Version}",
|
||||
$"Adapter version: {_adapterInfo.AdapterVersion}",
|
||||
$"Soneta.Business version: {_adapterInfo.SonetaBusinessVersion}",
|
||||
$"Soneta.Handel version: {_adapterInfo.SonetaHandelVersion}",
|
||||
$"Enova path: {_enovaPath}",
|
||||
$"Database: {_adapterInfo.DatabaseName}"
|
||||
};
|
||||
return Task.FromResult<object?>(string.Join(Environment.NewLine, lines));
|
||||
}
|
||||
|
||||
public Task<object?> GetLogAsync(RpcInvocation invocation, CancellationToken cancellationToken) =>
|
||||
ReadLogAsync(invocation, "out", cancellationToken);
|
||||
|
||||
public Task<object?> GetErrorLogAsync(RpcInvocation invocation, CancellationToken cancellationToken) =>
|
||||
ReadLogAsync(invocation, "err", cancellationToken);
|
||||
|
||||
private async Task<object?> ReadLogAsync(
|
||||
RpcInvocation invocation,
|
||||
string stream,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var date = invocation.Get<string>("date") ?? DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||||
if (!DatePattern().IsMatch(date))
|
||||
{
|
||||
throw WampErrorFactory.Create(
|
||||
"eu.smartb2b.sync.invalid_date",
|
||||
"Data musi mieć format YYYY-MM-DD.",
|
||||
new Dictionary<string, object?> { ["date"] = date });
|
||||
}
|
||||
|
||||
var compactDate = date.Replace("-", string.Empty, StringComparison.Ordinal);
|
||||
var path = Path.Combine(_logDirectory, $"{_logPrefix}_{compactDate}.{stream}.log");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[GeneratedRegex("^\\d{4}-\\d{2}-\\d{2}$", RegexOptions.CultureInvariant)]
|
||||
private static partial Regex DatePattern();
|
||||
}
|
||||
109
src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs
Normal file
109
src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Newtonsoft.Json;
|
||||
using SmartB2B.Enova.Contracts;
|
||||
using SmartB2B.Enova.Service.Rpc;
|
||||
|
||||
namespace SmartB2B.Enova.Service.Orders;
|
||||
|
||||
public sealed class PlaceOrderHandler
|
||||
{
|
||||
private readonly IEnovaOrderAdapter _adapter;
|
||||
private readonly EnovaAdapterConfiguration _configuration;
|
||||
private readonly SemaphoreSlim _orderLock = new(1, 1);
|
||||
|
||||
public PlaceOrderHandler(IEnovaOrderAdapter adapter, EnovaAdapterConfiguration configuration)
|
||||
{
|
||||
_adapter = adapter;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task<object?> HandleAsync(RpcInvocation invocation, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!invocation.Contains("currency_iso"))
|
||||
{
|
||||
throw WampErrorFactory.Create("eu.smartb2b.missing_currency", "Pole currency_iso jest wymagane.");
|
||||
}
|
||||
|
||||
var currency = invocation.Get<string>("currency_iso")?.Trim();
|
||||
if (!string.Equals(currency, "PLN", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw WampErrorFactory.Create(
|
||||
"eu.smartb2b.erp.currency_not_supported",
|
||||
$"Waluta '{currency}' nie jest obsługiwana. Wymagana waluta: PLN.",
|
||||
new Dictionary<string, object?> { ["currency_iso"] = currency });
|
||||
}
|
||||
|
||||
var inputLines = invocation.Get<PlaceOrderLine[]>("lines", []) ?? [];
|
||||
var request = new OrderRequest
|
||||
{
|
||||
CustomerCode = invocation.Get<string>("companyErpId") ?? string.Empty,
|
||||
CustomerReference = invocation.Get<string>("purchase_order_number"),
|
||||
Notes = invocation.Get<string>("notes"),
|
||||
DocumentDefinition = _configuration.DocumentDefinition,
|
||||
WarehouseCode = invocation.Get<string>("warehouseErpId") ?? _configuration.DefaultWarehouseCode,
|
||||
SaveMode = _configuration.SaveMode,
|
||||
Items = inputLines.Select(line => new OrderLineRequest
|
||||
{
|
||||
ProductCode = line.Symbol ?? string.Empty,
|
||||
Quantity = line.Quantity,
|
||||
UnitPrice = line.PriceNetto,
|
||||
Discount = line.Discount
|
||||
}).ToArray()
|
||||
};
|
||||
|
||||
var errors = OrderValidator.Validate(request);
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
throw WampErrorFactory.Create(
|
||||
"eu.smartb2b.erp.invalid_order_lines",
|
||||
string.Join(" | ", errors),
|
||||
new Dictionary<string, object?> { ["errors"] = errors });
|
||||
}
|
||||
|
||||
await _orderLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"Tworzenie ZO: kontrahent={request.CustomerCode}, pozycje={request.Items.Count}, referencja={request.CustomerReference ?? "(brak)"}.");
|
||||
|
||||
OrderResult result;
|
||||
try
|
||||
{
|
||||
result = await Task.Run(() => _adapter.CreateOrder(request), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (EnovaOperationException exception)
|
||||
{
|
||||
throw WampErrorFactory.FromEnova(exception);
|
||||
}
|
||||
|
||||
Console.WriteLine($"Utworzono ZO {result.Number} (ID: {result.Id}).");
|
||||
return new KeywordResult(new Dictionary<string, object?>
|
||||
{
|
||||
["order_erp_id"] = result.Id,
|
||||
["order_erp_symbol"] = result.Number,
|
||||
["value_netto"] = result.Net,
|
||||
["value_brutto"] = result.Gross,
|
||||
["stocks"] = Array.Empty<object>()
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_orderLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PlaceOrderLine
|
||||
{
|
||||
[JsonProperty("symbol")]
|
||||
public string? Symbol { get; init; }
|
||||
|
||||
[JsonProperty("quantity")]
|
||||
public decimal Quantity { get; init; }
|
||||
|
||||
[JsonProperty("price_netto")]
|
||||
public decimal PriceNetto { get; init; }
|
||||
|
||||
[JsonProperty("discount")]
|
||||
public decimal Discount { get; init; }
|
||||
}
|
||||
120
src/SmartB2B.Enova.Service/Program.cs
Normal file
120
src/SmartB2B.Enova.Service/Program.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using SmartB2B.Enova.Service.Configuration;
|
||||
using SmartB2B.Enova.Service.Diagnostics;
|
||||
using SmartB2B.Enova.Service.Orders;
|
||||
using SmartB2B.Enova.Service.Rpc;
|
||||
using SmartB2B.Enova.Service.Runtime;
|
||||
using SmartB2B.Enova.Service.Sql;
|
||||
using WampSharp.V2.Rpc;
|
||||
|
||||
namespace SmartB2B.Enova.Service;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
var cancellation = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += (_, eventArgs) =>
|
||||
{
|
||||
eventArgs.Cancel = true;
|
||||
cancellation.Cancel();
|
||||
};
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => cancellation.Cancel();
|
||||
|
||||
try
|
||||
{
|
||||
var commandLine = CommandLineOptions.Parse(args);
|
||||
var settings = ServiceSettings.Load(commandLine.ConfigurationPath);
|
||||
var runtime = settings.ResolveRuntimeSettings();
|
||||
|
||||
using var adapterLoader = new EnovaAdapterLoader(runtime.EnovaInstallationPath);
|
||||
var adapter = adapterLoader.Load();
|
||||
var adapterInfo = adapter.Initialize(runtime.EnovaConfiguration);
|
||||
|
||||
_ = new System.Data.Common.DbConnectionStringBuilder
|
||||
{
|
||||
ConnectionString = runtime.SqlConnectionString
|
||||
};
|
||||
var sql = new SqlRawHandler(
|
||||
runtime.SqlConnectionString,
|
||||
runtime.SqlCommandTimeoutSeconds);
|
||||
|
||||
Console.WriteLine(
|
||||
$"Załadowano Enovę {adapterInfo.SonetaBusinessVersion} z '{runtime.EnovaInstallationPath}'.");
|
||||
Console.WriteLine($"Baza: {adapterInfo.DatabaseName}; portal WAMP: {settings.Portal}.");
|
||||
|
||||
if (commandLine.CheckConfigurationOnly)
|
||||
{
|
||||
Console.WriteLine("Konfiguracja i adapter Enovy są poprawne.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var orders = new PlaceOrderHandler(adapter, runtime.EnovaConfiguration);
|
||||
var diagnostics = new DiagnosticsHandler(
|
||||
adapterInfo,
|
||||
runtime.EnovaInstallationPath,
|
||||
runtime.LogDirectory,
|
||||
runtime.LogPrefix);
|
||||
|
||||
IReadOnlyList<IWampRpcOperation> operations =
|
||||
[
|
||||
new DelegateRpcOperation("eu.smartb2b.place_order", orders.HandleAsync),
|
||||
new DelegateRpcOperation("eu.smartb2b.sql_raw", sql.HandleAsync),
|
||||
new DelegateRpcOperation("eu.smartb2b.sync.get_version", diagnostics.GetVersionAsync),
|
||||
new DelegateRpcOperation("eu.smartb2b.sync.get_info", diagnostics.GetInfoAsync),
|
||||
new DelegateRpcOperation("eu.smartb2b.sync.get_log", diagnostics.GetLogAsync),
|
||||
new DelegateRpcOperation("eu.smartb2b.sync.get_error_log", diagnostics.GetErrorLogAsync)
|
||||
];
|
||||
|
||||
var wampClient = new WampServiceClient(
|
||||
settings.Wamp.ServerUrl,
|
||||
settings.Portal,
|
||||
TimeSpan.FromSeconds(settings.Wamp.ReconnectDelaySeconds),
|
||||
operations);
|
||||
|
||||
await wampClient.RunAsync(cancellation.Token).ConfigureAwait(false);
|
||||
Console.WriteLine("SmartB2B Enova Sync zatrzymany.");
|
||||
return 0;
|
||||
}
|
||||
catch (ConfigurationException exception)
|
||||
{
|
||||
Console.Error.WriteLine($"Błąd konfiguracji: {exception.Message}");
|
||||
return 2;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"Błąd krytyczny: {exception}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record CommandLineOptions(string ConfigurationPath, bool CheckConfigurationOnly)
|
||||
{
|
||||
public static CommandLineOptions Parse(string[] args)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "config", "enova.json");
|
||||
var checkOnly = false;
|
||||
|
||||
for (var index = 0; index < args.Length; index++)
|
||||
{
|
||||
switch (args[index])
|
||||
{
|
||||
case "--config":
|
||||
if (++index >= args.Length || string.IsNullOrWhiteSpace(args[index]))
|
||||
{
|
||||
throw new ConfigurationException("Argument --config wymaga ścieżki.");
|
||||
}
|
||||
|
||||
path = Path.GetFullPath(args[index]);
|
||||
break;
|
||||
case "--check-config":
|
||||
checkOnly = true;
|
||||
break;
|
||||
default:
|
||||
throw new ConfigurationException($"Nieznany argument: {args[index]}");
|
||||
}
|
||||
}
|
||||
|
||||
return new CommandLineOptions(path, checkOnly);
|
||||
}
|
||||
}
|
||||
130
src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs
Normal file
130
src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using WampSharp.Core.Serialization;
|
||||
using WampSharp.V2.Core.Contracts;
|
||||
using WampSharp.V2.Rpc;
|
||||
|
||||
namespace SmartB2B.Enova.Service.Rpc;
|
||||
|
||||
public sealed class DelegateRpcOperation : IWampRpcOperation
|
||||
{
|
||||
private readonly Func<RpcInvocation, CancellationToken, Task<object?>> _handler;
|
||||
|
||||
public DelegateRpcOperation(
|
||||
string procedure,
|
||||
Func<RpcInvocation, CancellationToken, Task<object?>> handler)
|
||||
{
|
||||
Procedure = procedure;
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
public string Procedure { get; }
|
||||
|
||||
public IWampCancellableInvocation Invoke<TMessage>(
|
||||
IWampRawRpcOperationRouterCallback caller,
|
||||
IWampFormatter<TMessage> formatter,
|
||||
InvocationDetails details) =>
|
||||
Invoke(caller, formatter, details, [], new Dictionary<string, TMessage>());
|
||||
|
||||
public IWampCancellableInvocation Invoke<TMessage>(
|
||||
IWampRawRpcOperationRouterCallback caller,
|
||||
IWampFormatter<TMessage> formatter,
|
||||
InvocationDetails details,
|
||||
TMessage[] arguments) =>
|
||||
Invoke(caller, formatter, details, arguments, new Dictionary<string, TMessage>());
|
||||
|
||||
public IWampCancellableInvocation Invoke<TMessage>(
|
||||
IWampRawRpcOperationRouterCallback caller,
|
||||
IWampFormatter<TMessage> formatter,
|
||||
InvocationDetails details,
|
||||
TMessage[] arguments,
|
||||
IDictionary<string, TMessage> argumentsKeywords)
|
||||
{
|
||||
var cancellation = new CancellationTokenSource();
|
||||
_ = ExecuteAsync(
|
||||
caller,
|
||||
formatter,
|
||||
arguments ?? [],
|
||||
argumentsKeywords ?? new Dictionary<string, TMessage>(),
|
||||
cancellation.Token);
|
||||
return new CancellableInvocation(cancellation);
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync<TMessage>(
|
||||
IWampRawRpcOperationRouterCallback caller,
|
||||
IWampFormatter<TMessage> formatter,
|
||||
TMessage[] arguments,
|
||||
IDictionary<string, TMessage> argumentsKeywords,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var positional = arguments.Select(formatter.Deserialize<object>).ToArray();
|
||||
var keywords = argumentsKeywords.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => (object?)formatter.Deserialize<object>(pair.Value),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var result = await _handler(new RpcInvocation(positional, keywords), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var yieldOptions = new YieldOptions();
|
||||
|
||||
if (result is KeywordResult keywordResult)
|
||||
{
|
||||
var serializedKeywords = keywordResult.Values.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => formatter.Serialize(pair.Value),
|
||||
StringComparer.Ordinal);
|
||||
caller.Result(formatter, yieldOptions, [], serializedKeywords);
|
||||
}
|
||||
else if (result is null)
|
||||
{
|
||||
caller.Result(formatter, yieldOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
caller.Result(formatter, yieldOptions, [formatter.Serialize(result)]);
|
||||
}
|
||||
}
|
||||
catch (WampException exception)
|
||||
{
|
||||
var details = formatter.Serialize(exception.Details);
|
||||
var errorArguments = exception.Arguments.Select(formatter.Serialize).ToArray();
|
||||
var keywordArguments = formatter.Serialize(exception.ArgumentsKeywords);
|
||||
caller.Error(formatter, details, exception.ErrorUri, errorArguments, keywordArguments);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
var details = formatter.Serialize(new Dictionary<string, object>());
|
||||
caller.Error(formatter, details, "wamp.error.canceled");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"Nieobsłużony błąd procedury {Procedure}: {exception}");
|
||||
var details = formatter.Serialize(new Dictionary<string, object>());
|
||||
var keywordArguments = formatter.Serialize(new Dictionary<string, object?>
|
||||
{
|
||||
["message"] = exception.Message
|
||||
});
|
||||
caller.Error(
|
||||
formatter,
|
||||
details,
|
||||
"eu.smartb2b.error",
|
||||
Array.Empty<TMessage>(),
|
||||
keywordArguments);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CancellableInvocation : IWampCancellableInvocation
|
||||
{
|
||||
private readonly CancellationTokenSource _cancellation;
|
||||
|
||||
public CancellableInvocation(CancellationTokenSource cancellation)
|
||||
{
|
||||
_cancellation = cancellation;
|
||||
}
|
||||
|
||||
public void Cancel(InterruptDetails details)
|
||||
{
|
||||
_cancellation.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/SmartB2B.Enova.Service/Rpc/RpcInvocation.cs
Normal file
36
src/SmartB2B.Enova.Service/Rpc/RpcInvocation.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace SmartB2B.Enova.Service.Rpc;
|
||||
|
||||
public sealed class RpcInvocation
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, object?> _keywordArguments;
|
||||
|
||||
public RpcInvocation(object?[] positionalArguments, IReadOnlyDictionary<string, object?> keywordArguments)
|
||||
{
|
||||
PositionalArguments = positionalArguments;
|
||||
_keywordArguments = keywordArguments;
|
||||
}
|
||||
|
||||
public object?[] PositionalArguments { get; }
|
||||
|
||||
public bool Contains(string name) => _keywordArguments.ContainsKey(name);
|
||||
|
||||
public T? Get<T>(string name, T? defaultValue = default)
|
||||
{
|
||||
if (!_keywordArguments.TryGetValue(name, out var value) || value is null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value is T typed)
|
||||
{
|
||||
return typed;
|
||||
}
|
||||
|
||||
var token = value as JToken ?? JToken.FromObject(value);
|
||||
return token.ToObject<T>();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record KeywordResult(IReadOnlyDictionary<string, object?> Values);
|
||||
22
src/SmartB2B.Enova.Service/Rpc/WampErrorFactory.cs
Normal file
22
src/SmartB2B.Enova.Service/Rpc/WampErrorFactory.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using SmartB2B.Enova.Contracts;
|
||||
using WampSharp.V2.Core.Contracts;
|
||||
|
||||
namespace SmartB2B.Enova.Service.Rpc;
|
||||
|
||||
public static class WampErrorFactory
|
||||
{
|
||||
public static WampException FromEnova(EnovaOperationException exception) =>
|
||||
Create(exception.ErrorUri, exception.Message, exception.Details);
|
||||
|
||||
public static WampException Create(
|
||||
string errorUri,
|
||||
string message,
|
||||
IReadOnlyDictionary<string, object?>? details = null)
|
||||
{
|
||||
var keywordArguments = (details ?? new Dictionary<string, object?>())
|
||||
.ToDictionary(pair => pair.Key, pair => pair.Value!, StringComparer.Ordinal);
|
||||
keywordArguments["message"] = message;
|
||||
|
||||
return new WampException(errorUri, [], keywordArguments);
|
||||
}
|
||||
}
|
||||
99
src/SmartB2B.Enova.Service/Rpc/WampServiceClient.cs
Normal file
99
src/SmartB2B.Enova.Service/Rpc/WampServiceClient.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using WampSharp.V2;
|
||||
using WampSharp.V2.Client;
|
||||
using WampSharp.V2.Core.Contracts;
|
||||
using WampSharp.V2.Rpc;
|
||||
|
||||
namespace SmartB2B.Enova.Service.Rpc;
|
||||
|
||||
public sealed class WampServiceClient
|
||||
{
|
||||
private readonly string _serverUrl;
|
||||
private readonly string _realmName;
|
||||
private readonly TimeSpan _reconnectDelay;
|
||||
private readonly IReadOnlyList<IWampRpcOperation> _operations;
|
||||
|
||||
public WampServiceClient(
|
||||
string serverUrl,
|
||||
string realmName,
|
||||
TimeSpan reconnectDelay,
|
||||
IReadOnlyList<IWampRpcOperation> operations)
|
||||
{
|
||||
_serverUrl = serverUrl;
|
||||
_realmName = realmName;
|
||||
_reconnectDelay = reconnectDelay;
|
||||
_operations = operations;
|
||||
}
|
||||
|
||||
public async Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
IWampChannel? channel = null;
|
||||
var registrations = new List<IAsyncDisposable>();
|
||||
try
|
||||
{
|
||||
var channelFactory = new DefaultWampChannelFactory();
|
||||
channel = channelFactory.CreateJsonChannel(_serverUrl, _realmName);
|
||||
var disconnected = new TaskCompletionSource(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
channel.RealmProxy.Monitor.ConnectionBroken += (_, _) => disconnected.TrySetResult();
|
||||
channel.RealmProxy.Monitor.ConnectionError += (_, _) => disconnected.TrySetResult();
|
||||
|
||||
Console.WriteLine($"Łączenie z WAMP: realm={_realmName}, url={_serverUrl}.");
|
||||
await channel.Open().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var operation in _operations)
|
||||
{
|
||||
var registration = await channel.RealmProxy.RpcCatalog.Register(
|
||||
operation,
|
||||
new RegisterOptions { Invoke = "last" })
|
||||
.WaitAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
registrations.Add(registration);
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
$"Połączono z WAMP; zarejestrowano {_operations.Count} procedur.");
|
||||
await disconnected.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
Console.Error.WriteLine("Połączenie WAMP zostało przerwane.");
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"Błąd połączenia WAMP: {exception.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var registration in registrations)
|
||||
{
|
||||
try
|
||||
{
|
||||
await registration.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Sesja może już nie istnieć po zerwaniu połączenia.
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
channel?.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Zamknięcie uszkodzonego kanału nie może zatrzymać pętli ponownego łączenia.
|
||||
}
|
||||
}
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(_reconnectDelay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/SmartB2B.Enova.Service/Runtime/EnovaAdapterLoader.cs
Normal file
115
src/SmartB2B.Enova.Service/Runtime/EnovaAdapterLoader.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using SmartB2B.Enova.Contracts;
|
||||
|
||||
namespace SmartB2B.Enova.Service.Runtime;
|
||||
|
||||
public sealed class EnovaAdapterLoader : IDisposable
|
||||
{
|
||||
private static readonly string[] RequiredAssemblies =
|
||||
[
|
||||
"Soneta.Start.dll",
|
||||
"Soneta.Business.dll",
|
||||
"Soneta.Handel.dll",
|
||||
"Soneta.CRM.dll"
|
||||
];
|
||||
|
||||
private readonly string _enovaPath;
|
||||
private readonly string _adapterPath;
|
||||
private readonly object _syncRoot = new();
|
||||
private readonly Dictionary<string, Assembly> _resolved = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public EnovaAdapterLoader(string enovaPath, string? adapterPath = null)
|
||||
{
|
||||
_enovaPath = Path.GetFullPath(enovaPath);
|
||||
_adapterPath = Path.GetFullPath(adapterPath ?? Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
"adapters",
|
||||
"SmartB2B.Enova.Adapter.dll"));
|
||||
|
||||
ValidateFiles();
|
||||
AssemblyLoadContext.Default.Resolving += ResolveAssembly;
|
||||
|
||||
var currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
||||
if (!currentPath.Split(Path.PathSeparator).Contains(_enovaPath, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("PATH", _enovaPath + Path.PathSeparator + currentPath);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnovaOrderAdapter Load()
|
||||
{
|
||||
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(_adapterPath);
|
||||
var adapterType = assembly.GetExportedTypes().SingleOrDefault(type =>
|
||||
!type.IsAbstract && typeof(IEnovaOrderAdapter).IsAssignableFrom(type));
|
||||
|
||||
if (adapterType is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"W module '{_adapterPath}' nie znaleziono implementacji {nameof(IEnovaOrderAdapter)}.");
|
||||
}
|
||||
|
||||
return (IEnovaOrderAdapter)(Activator.CreateInstance(adapterType)
|
||||
?? throw new InvalidOperationException($"Nie można utworzyć adaptera '{adapterType.FullName}'."));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
AssemblyLoadContext.Default.Resolving -= ResolveAssembly;
|
||||
}
|
||||
|
||||
private Assembly? ResolveAssembly(AssemblyLoadContext context, AssemblyName assemblyName)
|
||||
{
|
||||
var simpleName = assemblyName.Name;
|
||||
if (string.IsNullOrWhiteSpace(simpleName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_resolved.TryGetValue(simpleName, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(Path.GetDirectoryName(_adapterPath)!, simpleName + ".dll"),
|
||||
Path.Combine(_enovaPath, simpleName + ".dll")
|
||||
};
|
||||
|
||||
var path = candidates.FirstOrDefault(File.Exists);
|
||||
if (path is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var loaded = context.LoadFromAssemblyPath(path);
|
||||
_resolved[simpleName] = loaded;
|
||||
return loaded;
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateFiles()
|
||||
{
|
||||
if (!Directory.Exists(_enovaPath))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"Nie znaleziono katalogu instalacji Enovy: {_enovaPath}");
|
||||
}
|
||||
|
||||
if (!File.Exists(_adapterPath))
|
||||
{
|
||||
throw new FileNotFoundException("Nie znaleziono modułu adaptera Enovy.", _adapterPath);
|
||||
}
|
||||
|
||||
var missing = RequiredAssemblies
|
||||
.Where(file => !File.Exists(Path.Combine(_enovaPath, file)))
|
||||
.ToArray();
|
||||
if (missing.Length > 0)
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
$"W katalogu Enovy brakuje wymaganych bibliotek: {string.Join(", ", missing)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/SmartB2B.Enova.Service/SmartB2B.Enova.Service.csproj
Normal file
29
src/SmartB2B.Enova.Service/SmartB2B.Enova.Service.csproj
Normal file
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>SmartB2B.Enova.Service</AssemblyName>
|
||||
<Version>1.0.0</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../SmartB2B.Enova.Contracts/SmartB2B.Enova.Contracts.csproj" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="WampSharp.Default.Client" Version="23.8.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="config/enova.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
<Target Name="BuildEnovaAdapter" BeforeTargets="Build">
|
||||
<MSBuild Projects="../SmartB2B.Enova.Adapter/SmartB2B.Enova.Adapter.csproj" Targets="Build" Properties="Configuration=$(Configuration)" />
|
||||
</Target>
|
||||
<Target Name="CopyEnovaAdapter" AfterTargets="Build" DependsOnTargets="BuildEnovaAdapter">
|
||||
<MakeDir Directories="$(OutDir)adapters" />
|
||||
<Copy SourceFiles="../SmartB2B.Enova.Adapter/bin/$(Configuration)/net8.0/SmartB2B.Enova.Adapter.dll" DestinationFolder="$(OutDir)adapters" />
|
||||
</Target>
|
||||
<Target Name="CopyEnovaAdapterToPublish" AfterTargets="Publish" DependsOnTargets="BuildEnovaAdapter">
|
||||
<MakeDir Directories="$(PublishDir)adapters" />
|
||||
<Copy SourceFiles="../SmartB2B.Enova.Adapter/bin/$(Configuration)/net8.0/SmartB2B.Enova.Adapter.dll" DestinationFolder="$(PublishDir)adapters" />
|
||||
</Target>
|
||||
</Project>
|
||||
157
src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs
Normal file
157
src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using System.Data.Common;
|
||||
using Newtonsoft.Json;
|
||||
using SmartB2B.Enova.Service.Rpc;
|
||||
|
||||
namespace SmartB2B.Enova.Service.Sql;
|
||||
|
||||
public sealed class SqlRawHandler
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private readonly int _commandTimeoutSeconds;
|
||||
private readonly Type _connectionType;
|
||||
|
||||
public SqlRawHandler(string connectionString, int commandTimeoutSeconds)
|
||||
{
|
||||
_ = new DbConnectionStringBuilder { ConnectionString = connectionString };
|
||||
_connectionString = connectionString;
|
||||
_commandTimeoutSeconds = commandTimeoutSeconds;
|
||||
_connectionType = ResolveConnectionType();
|
||||
}
|
||||
|
||||
public async Task<object?> HandleAsync(RpcInvocation invocation, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = invocation.Get<string>("query");
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
throw WampErrorFactory.Create("eu.smartb2b.sql_error", "Pole query jest wymagane.");
|
||||
}
|
||||
|
||||
var parameters = invocation.Get<object?[]>("params", []) ?? [];
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = CreateConnection();
|
||||
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = query;
|
||||
command.CommandTimeout = _commandTimeoutSeconds;
|
||||
|
||||
for (var index = 0; index < parameters.Length; index++)
|
||||
{
|
||||
var parameter = command.CreateParameter();
|
||||
parameter.ParameterName = $"@{index + 1}";
|
||||
parameter.Value = NormalizeParameter(parameters[index]);
|
||||
command.Parameters.Add(parameter);
|
||||
}
|
||||
|
||||
var rowsAffected = new List<int>();
|
||||
|
||||
var recordsets = new List<IReadOnlyList<IReadOnlyDictionary<string, object?>>>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
do
|
||||
{
|
||||
if (reader.FieldCount == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var rows = new List<IReadOnlyDictionary<string, object?>>();
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var row = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
for (var ordinal = 0; ordinal < reader.FieldCount; ordinal++)
|
||||
{
|
||||
var name = reader.GetName(ordinal);
|
||||
if (row.ContainsKey(name))
|
||||
{
|
||||
name = $"{name}_{ordinal + 1}";
|
||||
}
|
||||
|
||||
row[name] = await reader.IsDBNullAsync(ordinal, cancellationToken).ConfigureAwait(false)
|
||||
? null
|
||||
: NormalizeResultValue(reader.GetValue(ordinal));
|
||||
}
|
||||
|
||||
rows.Add(row);
|
||||
}
|
||||
|
||||
recordsets.Add(rows);
|
||||
}
|
||||
while (await reader.NextResultAsync(cancellationToken).ConfigureAwait(false));
|
||||
|
||||
if (reader.RecordsAffected >= 0)
|
||||
{
|
||||
rowsAffected.Add(reader.RecordsAffected);
|
||||
}
|
||||
|
||||
return new SqlRawResult
|
||||
{
|
||||
Recordsets = recordsets,
|
||||
Recordset = recordsets.FirstOrDefault(),
|
||||
Output = new Dictionary<string, object?>(),
|
||||
RowsAffected = rowsAffected
|
||||
};
|
||||
}
|
||||
catch (DbException exception)
|
||||
{
|
||||
var code = exception.GetType().GetProperty("Number")?.GetValue(exception)
|
||||
?? exception.ErrorCode;
|
||||
Console.Error.WriteLine($"Błąd SQL {code}: {exception.Message}");
|
||||
throw WampErrorFactory.Create(
|
||||
"eu.smartb2b.sql_error",
|
||||
exception.Message,
|
||||
new Dictionary<string, object?> { ["code"] = code });
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw WampErrorFactory.Create("eu.smartb2b.sql_error", "Przekroczono czas wykonania zapytania SQL.");
|
||||
}
|
||||
}
|
||||
|
||||
private DbConnection CreateConnection()
|
||||
{
|
||||
return (DbConnection)(Activator.CreateInstance(_connectionType, _connectionString)
|
||||
?? throw new InvalidOperationException("Nie można utworzyć połączenia SQL."));
|
||||
}
|
||||
|
||||
private static Type ResolveConnectionType() =>
|
||||
Type.GetType(
|
||||
"System.Data.SqlClient.SqlConnection, System.Data.SqlClient",
|
||||
throwOnError: true)
|
||||
?? throw new InvalidOperationException("Nie znaleziono dostawcy System.Data.SqlClient.");
|
||||
|
||||
private static object NormalizeParameter(object? value)
|
||||
{
|
||||
if (value is Newtonsoft.Json.Linq.JValue jsonValue)
|
||||
{
|
||||
value = jsonValue.Value;
|
||||
}
|
||||
|
||||
return value ?? DBNull.Value;
|
||||
}
|
||||
|
||||
private static object NormalizeResultValue(object value) =>
|
||||
value is byte[] bytes
|
||||
? new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "Buffer",
|
||||
["data"] = bytes.Select(item => (int)item).ToArray()
|
||||
}
|
||||
: value;
|
||||
}
|
||||
|
||||
public sealed class SqlRawResult
|
||||
{
|
||||
[JsonProperty("recordsets")]
|
||||
public IReadOnlyList<IReadOnlyList<IReadOnlyDictionary<string, object?>>> Recordsets { get; init; } = [];
|
||||
|
||||
[JsonProperty("recordset", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public IReadOnlyList<IReadOnlyDictionary<string, object?>>? Recordset { get; init; }
|
||||
|
||||
[JsonProperty("output")]
|
||||
public IReadOnlyDictionary<string, object?> Output { get; init; } = new Dictionary<string, object?>();
|
||||
|
||||
[JsonProperty("rowsAffected")]
|
||||
public IReadOnlyList<int> RowsAffected { get; init; } = [];
|
||||
}
|
||||
24
src/SmartB2B.Enova.Service/config/enova.json
Normal file
24
src/SmartB2B.Enova.Service/config/enova.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"portal": "demo",
|
||||
"wamp": {
|
||||
"serverUrl": "ws://wamp.smartb2b.eu:8080/",
|
||||
"reconnectDelaySeconds": 10
|
||||
},
|
||||
"enova": {
|
||||
"installationPath": "C:\\Program Files (x86)\\Soneta\\enova365 2604.4.4",
|
||||
"database": "Firma demo",
|
||||
"operator": "Administrator",
|
||||
"passwordEnvironmentVariable": "SMARTB2B_ENOVA_PASSWORD",
|
||||
"documentDefinition": "ZO",
|
||||
"defaultWarehouseCode": null,
|
||||
"saveMode": "Buffer"
|
||||
},
|
||||
"sql": {
|
||||
"connectionStringEnvironmentVariable": "SMARTB2B_SQL_CONNECTION_STRING",
|
||||
"commandTimeoutSeconds": 30
|
||||
},
|
||||
"diagnostics": {
|
||||
"logDirectory": "daemon",
|
||||
"logPrefix": "smartb2bsync-enova"
|
||||
}
|
||||
}
|
||||
343
tests/SmartB2B.Enova.Tests/Program.cs
Normal file
343
tests/SmartB2B.Enova.Tests/Program.cs
Normal file
@@ -0,0 +1,343 @@
|
||||
using SmartB2B.Enova.Contracts;
|
||||
using SmartB2B.Enova.Service.Orders;
|
||||
using SmartB2B.Enova.Service.Rpc;
|
||||
using SmartB2B.Enova.Service.Runtime;
|
||||
using WampSharp.Core.Serialization;
|
||||
using WampSharp.V2.Core;
|
||||
using WampSharp.V2.Core.Contracts;
|
||||
using WampSharp.V2.Rpc;
|
||||
|
||||
var tests = new (string Name, Func<Task> Run)[]
|
||||
{
|
||||
("Poprawne zamówienie z rabatem ułamkowym", ValidFractionalDiscount),
|
||||
("Rabat 100 procent jest dozwolony", FullDiscount),
|
||||
("Rabat ujemny jest odrzucany", NegativeDiscount),
|
||||
("Rabat powyżej 100 jest odrzucany", DiscountAboveOneHundred),
|
||||
("Ilość zerowa jest odrzucana", ZeroQuantity),
|
||||
("Cena ujemna jest odrzucana", NegativePrice),
|
||||
("Brak towaru jest odrzucany", MissingProduct),
|
||||
("Brak pozycji jest odrzucany", MissingItems),
|
||||
("Powtarzająca się referencja nie jest walidowana", ReferenceMayRepeat),
|
||||
("Kontrakt place_order jest mapowany na Enovę", PlaceOrderMapping),
|
||||
("place_order zwraca nazwany wynik", PlaceOrderKeywordResult),
|
||||
("Warstwa WAMP wysyła wynik jako kwargs", WampKeywordWireResult),
|
||||
("Brak waluty zwraca stabilny błąd", MissingCurrency),
|
||||
("Waluta inna niż PLN jest odrzucana", UnsupportedCurrency),
|
||||
("Brak katalogu Enovy jest wykrywany", MissingEnovaDirectory)
|
||||
};
|
||||
|
||||
var failures = 0;
|
||||
foreach (var test in tests)
|
||||
{
|
||||
try
|
||||
{
|
||||
await test.Run();
|
||||
Console.WriteLine($"PASS: {test.Name}");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
failures++;
|
||||
Console.Error.WriteLine($"FAIL: {test.Name}: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"Wynik: {tests.Length - failures}/{tests.Length} testów zaliczonych.");
|
||||
return failures == 0 ? 0 : 1;
|
||||
|
||||
static Task ValidFractionalDiscount()
|
||||
{
|
||||
AssertNoErrors(CreateValidRequest(12.5m));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task FullDiscount()
|
||||
{
|
||||
AssertNoErrors(CreateValidRequest(100m));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task NegativeDiscount()
|
||||
{
|
||||
AssertHasError(CreateValidRequest(-0.01m), "discount");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task DiscountAboveOneHundred()
|
||||
{
|
||||
AssertHasError(CreateValidRequest(100.01m), "discount");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task ZeroQuantity()
|
||||
{
|
||||
AssertHasError(CreateValidRequest(quantity: 0m), "quantity");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task NegativePrice()
|
||||
{
|
||||
AssertHasError(CreateValidRequest(unitPrice: -0.01m), "price_netto");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task MissingProduct()
|
||||
{
|
||||
AssertHasError(CreateValidRequest(productCode: ""), "symbol");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task MissingItems()
|
||||
{
|
||||
var request = new OrderRequest { CustomerCode = "Abc", Items = [] };
|
||||
AssertHasError(request, "co najmniej jedną");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task ReferenceMayRepeat()
|
||||
{
|
||||
AssertNoErrors(CreateValidRequest());
|
||||
AssertNoErrors(CreateValidRequest());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static async Task PlaceOrderMapping()
|
||||
{
|
||||
var adapter = new FakeAdapter();
|
||||
var handler = new PlaceOrderHandler(adapter, CreateConfiguration());
|
||||
var invocation = CreateInvocation();
|
||||
|
||||
await handler.HandleAsync(invocation, CancellationToken.None);
|
||||
|
||||
var request = adapter.LastRequest ?? throw new InvalidOperationException("Adapter nie otrzymał zamówienia.");
|
||||
AssertEqual("ABC", request.CustomerCode, "companyErpId");
|
||||
AssertEqual("MAG", request.WarehouseCode, "warehouseErpId");
|
||||
AssertEqual("PO/1", request.CustomerReference, "purchase_order_number");
|
||||
AssertEqual(2, request.Items.Count, "liczba pozycji");
|
||||
AssertEqual(12.5m, request.Items[0].Discount, "rabat");
|
||||
AssertEqual(120m, request.Items[0].UnitPrice, "cena netto");
|
||||
}
|
||||
|
||||
static async Task PlaceOrderKeywordResult()
|
||||
{
|
||||
var handler = new PlaceOrderHandler(new FakeAdapter(), CreateConfiguration());
|
||||
var result = await handler.HandleAsync(CreateInvocation(), CancellationToken.None);
|
||||
var keywords = result as KeywordResult
|
||||
?? throw new InvalidOperationException("Wynik nie jest wynikiem nazwanym WAMP.");
|
||||
|
||||
AssertEqual("ZO/1", keywords.Values["order_erp_symbol"], "order_erp_symbol");
|
||||
AssertEqual(100m, keywords.Values["value_netto"], "value_netto");
|
||||
AssertEqual(123m, keywords.Values["value_brutto"], "value_brutto");
|
||||
}
|
||||
|
||||
static async Task WampKeywordWireResult()
|
||||
{
|
||||
var operation = new DelegateRpcOperation(
|
||||
"test.keyword",
|
||||
(_, _) => Task.FromResult<object?>(new KeywordResult(
|
||||
new Dictionary<string, object?> { ["answer"] = 42 })));
|
||||
var callback = new FakeRouterCallback();
|
||||
|
||||
_ = operation.Invoke(
|
||||
callback,
|
||||
WampObjectFormatter.Value,
|
||||
new InvocationDetails(),
|
||||
Array.Empty<object>(),
|
||||
new Dictionary<string, object>());
|
||||
|
||||
var result = await callback.Completion.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
AssertEqual(0, result.Arguments.Length, "liczba argumentów pozycyjnych WAMP");
|
||||
AssertEqual(42, result.KeywordArguments["answer"], "argument nazwany WAMP");
|
||||
}
|
||||
|
||||
static async Task MissingCurrency()
|
||||
{
|
||||
var handler = new PlaceOrderHandler(new FakeAdapter(), CreateConfiguration());
|
||||
var invocation = new RpcInvocation([], new Dictionary<string, object?>());
|
||||
await AssertWampError(handler.HandleAsync(invocation, CancellationToken.None), "eu.smartb2b.missing_currency");
|
||||
}
|
||||
|
||||
static async Task UnsupportedCurrency()
|
||||
{
|
||||
var handler = new PlaceOrderHandler(new FakeAdapter(), CreateConfiguration());
|
||||
var invocation = new RpcInvocation([], new Dictionary<string, object?> { ["currency_iso"] = "EUR" });
|
||||
await AssertWampError(
|
||||
handler.HandleAsync(invocation, CancellationToken.None),
|
||||
"eu.smartb2b.erp.currency_not_supported");
|
||||
}
|
||||
|
||||
static Task MissingEnovaDirectory()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
_ = new EnovaAdapterLoader(path, Path.Combine(path, "adapter.dll"));
|
||||
throw new InvalidOperationException("Oczekiwano DirectoryNotFoundException.");
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
static RpcInvocation CreateInvocation() => new(
|
||||
[],
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["currency_iso"] = "PLN",
|
||||
["companyErpId"] = "ABC",
|
||||
["warehouseErpId"] = "MAG",
|
||||
["purchase_order_number"] = "PO/1",
|
||||
["notes"] = "Test",
|
||||
["lines"] = new[]
|
||||
{
|
||||
new PlaceOrderLine { Symbol = "T1", Quantity = 2m, PriceNetto = 120m, Discount = 12.5m },
|
||||
new PlaceOrderLine { Symbol = "T2", Quantity = 1m, PriceNetto = 50m, Discount = 0m }
|
||||
}
|
||||
});
|
||||
|
||||
static EnovaAdapterConfiguration CreateConfiguration() => new(
|
||||
new EnovaConnectionOptions("Firma demo", "Administrator", ""),
|
||||
"ZO",
|
||||
null,
|
||||
OrderSaveMode.Buffer);
|
||||
|
||||
static OrderRequest CreateValidRequest(
|
||||
decimal discount = 0m,
|
||||
decimal quantity = 1m,
|
||||
decimal unitPrice = 100m,
|
||||
string productCode = "BIKINI") =>
|
||||
new()
|
||||
{
|
||||
CustomerCode = "Abc",
|
||||
CustomerReference = "KLIENT/TEST/1",
|
||||
Notes = "Test",
|
||||
DocumentDefinition = "ZO",
|
||||
SaveMode = OrderSaveMode.Buffer,
|
||||
Items =
|
||||
[
|
||||
new OrderLineRequest
|
||||
{
|
||||
ProductCode = productCode,
|
||||
Quantity = quantity,
|
||||
UnitPrice = unitPrice,
|
||||
Discount = discount
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
static void AssertNoErrors(OrderRequest request)
|
||||
{
|
||||
var errors = OrderValidator.Validate(request);
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(string.Join(" | ", errors));
|
||||
}
|
||||
}
|
||||
|
||||
static void AssertHasError(OrderRequest request, string expectedText)
|
||||
{
|
||||
var errors = OrderValidator.Validate(request);
|
||||
if (!errors.Any(error => error.Contains(expectedText, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Oczekiwano błędu zawierającego '{expectedText}', otrzymano: {string.Join(" | ", errors)}");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task AssertWampError(Task<object?> task, string expectedUri)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
throw new InvalidOperationException($"Oczekiwano błędu {expectedUri}.");
|
||||
}
|
||||
catch (WampException exception) when (exception.ErrorUri == expectedUri)
|
||||
{
|
||||
// Oczekiwany błąd.
|
||||
}
|
||||
}
|
||||
|
||||
static void AssertEqual(object? expected, object? actual, string name)
|
||||
{
|
||||
if (!Equals(expected, actual))
|
||||
{
|
||||
throw new InvalidOperationException($"{name}: oczekiwano '{expected}', otrzymano '{actual}'.");
|
||||
}
|
||||
}
|
||||
|
||||
file sealed class FakeAdapter : IEnovaOrderAdapter
|
||||
{
|
||||
public OrderRequest? LastRequest { get; private set; }
|
||||
|
||||
public EnovaAdapterInfo Initialize(EnovaAdapterConfiguration configuration) =>
|
||||
new("1.0.0", "2604.4.4.0", "2604.4.4.0", configuration.Connection.DatabaseName);
|
||||
|
||||
public OrderResult CreateOrder(OrderRequest request)
|
||||
{
|
||||
LastRequest = request;
|
||||
return new OrderResult(
|
||||
1,
|
||||
"ZO/1",
|
||||
request.SaveMode,
|
||||
request.CustomerCode,
|
||||
request.WarehouseCode ?? "FIRMA",
|
||||
request.CustomerReference,
|
||||
request.Items.Count,
|
||||
100m,
|
||||
23m,
|
||||
123m,
|
||||
request.Items.Any(line => line.Discount > 0));
|
||||
}
|
||||
}
|
||||
|
||||
file sealed class FakeRouterCallback : IWampRawRpcOperationRouterCallback
|
||||
{
|
||||
public long RequestId => 1;
|
||||
|
||||
public TaskCompletionSource<CapturedResult> Completion { get; } =
|
||||
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public void Result<TMessage>(IWampFormatter<TMessage> formatter, YieldOptions details) =>
|
||||
Completion.TrySetResult(new CapturedResult([], new Dictionary<string, object?>()));
|
||||
|
||||
public void Result<TMessage>(
|
||||
IWampFormatter<TMessage> formatter,
|
||||
YieldOptions details,
|
||||
TMessage[] arguments) =>
|
||||
Completion.TrySetResult(new CapturedResult(
|
||||
arguments.Select(formatter.Deserialize<object>).ToArray(),
|
||||
new Dictionary<string, object?>()));
|
||||
|
||||
public void Result<TMessage>(
|
||||
IWampFormatter<TMessage> formatter,
|
||||
YieldOptions details,
|
||||
TMessage[] arguments,
|
||||
IDictionary<string, TMessage> argumentsKeywords) =>
|
||||
Completion.TrySetResult(new CapturedResult(
|
||||
arguments.Select(formatter.Deserialize<object>).ToArray(),
|
||||
argumentsKeywords.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => (object?)formatter.Deserialize<object>(pair.Value))));
|
||||
|
||||
public void Error<TMessage>(IWampFormatter<TMessage> formatter, TMessage details, string error) =>
|
||||
Completion.TrySetException(new InvalidOperationException(error));
|
||||
|
||||
public void Error<TMessage>(
|
||||
IWampFormatter<TMessage> formatter,
|
||||
TMessage details,
|
||||
string error,
|
||||
TMessage[] arguments) =>
|
||||
Completion.TrySetException(new InvalidOperationException(error));
|
||||
|
||||
public void Error<TMessage>(
|
||||
IWampFormatter<TMessage> formatter,
|
||||
TMessage details,
|
||||
string error,
|
||||
TMessage[] arguments,
|
||||
TMessage argumentsKeywords) =>
|
||||
Completion.TrySetException(new InvalidOperationException(error));
|
||||
}
|
||||
|
||||
file sealed record CapturedResult(
|
||||
object?[] Arguments,
|
||||
IReadOnlyDictionary<string, object?> KeywordArguments);
|
||||
12
tests/SmartB2B.Enova.Tests/SmartB2B.Enova.Tests.csproj
Normal file
12
tests/SmartB2B.Enova.Tests/SmartB2B.Enova.Tests.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/SmartB2B.Enova.Contracts/SmartB2B.Enova.Contracts.csproj" />
|
||||
<ProjectReference Include="../../src/SmartB2B.Enova.Service/SmartB2B.Enova.Service.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user