Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/extraScripts/migration/holoo2hesabix/Services/HesabixApiClient.vb
Babak Alizadeh a5875e7a3a Release Holoo2Hesabix 1.1.0 with production-ready accounting migration.
Harden Holoo-to-Hesabix document partition/mapping, sample E2E tooling, remove hardcoded SQL credentials from scripts, fix income AR account 10401, and publish Setup MSI 1.1.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 15:56:40 +03:30

901 lines
41 KiB
VB.net
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Imports System.Net
Imports System.Net.Http
Imports System.Net.Http.Headers
Imports System.Text
Imports System.Threading
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq
Friend Class HesabixApiException
Inherits Exception
Public ReadOnly Property StatusCode As Integer
Public ReadOnly Property ErrorCode As String
Public Sub New(message As String, Optional statusCode As Integer = 0, Optional errorCode As String = Nothing, Optional inner As Exception = Nothing)
MyBase.New(message, inner)
Me.StatusCode = statusCode
Me.ErrorCode = errorCode
End Sub
End Class
Friend Class HesabixApiClient
Implements IDisposable
Private ReadOnly _http As HttpClient
Private ReadOnly _sync As New Object()
Private _disposed As Boolean
Private _baseUrl As String = ""
Private _apiKey As String = ""
Public Sub New()
Dim handler As New HttpClientHandler()
handler.AutomaticDecompression = DecompressionMethods.GZip Or DecompressionMethods.Deflate
Try
ServicePointManager.DefaultConnectionLimit = Math.Max(ServicePointManager.DefaultConnectionLimit, 32)
ServicePointManager.Expect100Continue = False
Catch
End Try
_http = New HttpClient(handler)
' هر فاکتور جداگانه؛ ۱۰ دقیقه برای یک سند کافی است
_http.Timeout = TimeSpan.FromMinutes(10)
_http.DefaultRequestHeaders.Accept.Clear()
_http.DefaultRequestHeaders.Accept.Add(New MediaTypeWithQualityHeaderValue("application/json"))
' BaseAddress عمداً تنظیم نمی‌شود؛ بعد از اولین درخواست قابل تغییر نیست.
End Sub
Public Sub SetBaseUrl(baseUrl As String)
If String.IsNullOrWhiteSpace(baseUrl) Then
Throw New ArgumentException("آدرس سرور حسابیکس خالی است.")
End If
SyncLock _sync
_baseUrl = baseUrl.Trim().TrimEnd("/"c)
Try
Dim sp = ServicePointManager.FindServicePoint(New Uri(_baseUrl & "/"))
If sp IsNot Nothing Then sp.ConnectionLimit = Math.Max(sp.ConnectionLimit, 32)
Catch
End Try
End SyncLock
End Sub
Public Sub Configure(baseUrl As String, apiKey As String)
SetBaseUrl(baseUrl)
If String.IsNullOrWhiteSpace(apiKey) Then
Throw New ArgumentException("کلید API خالی است.")
End If
SetApiKey(apiKey)
End Sub
Public Sub SetApiKey(apiKey As String)
SyncLock _sync
_apiKey = If(apiKey, "").Trim()
End SyncLock
End Sub
Public Sub ClearApiKey()
SyncLock _sync
_apiKey = ""
End SyncLock
End Sub
Public Async Function GetCaptchaAsync(Optional ct As CancellationToken = Nothing) As Task(Of CaptchaChallenge)
ClearApiKey()
Dim root = Await SendJsonAsync(HttpMethod.Post, "api/v1/auth/captcha", Nothing, includeAuth:=False, ct:=ct).ConfigureAwait(False)
Dim data = GetDataToken(root)
Return New CaptchaChallenge With {
.CaptchaId = GetString(data, "captcha_id"),
.ImageBase64 = GetString(data, "image_base64"),
.TtlSeconds = GetInt(data, "ttl_seconds")
}
End Function
Public Async Function LoginAsync(identifier As String, password As String, captchaId As String, captchaCode As String, Optional ct As CancellationToken = Nothing) As Task(Of LoginResult)
ClearApiKey()
Dim payload As New JObject From {
{"identifier", identifier},
{"password", password},
{"captcha_id", captchaId},
{"captcha_code", captchaCode},
{"device_id", "holoo2hesabix-desktop"}
}
Dim root = Await SendJsonAsync(HttpMethod.Post, "api/v1/auth/login", payload, includeAuth:=False, ct:=ct).ConfigureAwait(False)
Dim data = GetDataToken(root)
Dim apiKey = GetString(data, "api_key")
If String.IsNullOrWhiteSpace(apiKey) Then
Throw New HesabixApiException("سرور کلید API برنگرداند.")
End If
Dim userToken = data("user")
Dim user As HesabixUser
If userToken IsNot Nothing AndAlso userToken.Type = JTokenType.Object Then
user = ParseUser(DirectCast(userToken, JObject))
Else
user = New HesabixUser()
End If
SetApiKey(apiKey)
Return New LoginResult With {.ApiKey = apiKey, .User = user}
End Function
Public Async Function GetMeAsync(Optional ct As CancellationToken = Nothing) As Task(Of HesabixUser)
Dim root = Await GetJsonAsync("api/v1/auth/me", ct).ConfigureAwait(False)
Return ParseUser(GetDataToken(root))
End Function
Public Async Function ListBusinessesAsync(Optional take As Integer = 100, Optional ct As CancellationToken = Nothing) As Task(Of List(Of HesabixBusiness))
Dim root = Await SendJsonAsync(HttpMethod.Post, "api/v1/businesses/list?take=" & take.ToString() & "&skip=0", Nothing, includeAuth:=True, ct:=ct).ConfigureAwait(False)
Dim data = GetDataToken(root)
Dim items = GetItemsArray(data)
Dim list As New List(Of HesabixBusiness)
For Each item As JToken In items
Dim obj = TryCast(item, JObject)
If obj Is Nothing Then Continue For
list.Add(New HesabixBusiness With {
.Id = GetInt(obj, "id"),
.Name = GetString(obj, "name"),
.BusinessType = GetString(obj, "business_type"),
.BusinessField = GetString(obj, "business_field"),
.OwnerId = GetNullableInt(obj, "owner_id"),
.CreatedAt = GetString(obj, "created_at")
})
Next
Return list
End Function
Public Async Function ListCurrenciesAsync(Optional ct As CancellationToken = Nothing) As Task(Of List(Of CurrencyInfo))
Dim root = Await GetJsonAsync("api/v1/currencies", ct).ConfigureAwait(False)
Dim dataToken = root("data")
Dim list As New List(Of CurrencyInfo)
Dim items As JArray
If dataToken IsNot Nothing AndAlso dataToken.Type = JTokenType.Array Then
items = DirectCast(dataToken, JArray)
ElseIf dataToken IsNot Nothing AndAlso dataToken.Type = JTokenType.Object Then
items = GetItemsArray(DirectCast(dataToken, JObject))
Else
items = New JArray()
End If
For Each item As JToken In items
Dim obj = TryCast(item, JObject)
If obj Is Nothing Then Continue For
list.Add(ParseCurrency(obj))
Next
Return list
End Function
Public Async Function CreateBusinessAsync(request As NewBusinessRequest, Optional ct As CancellationToken = Nothing) As Task(Of HesabixBusiness)
Dim payload As New JObject From {
{"name", request.Name},
{"business_type", request.BusinessType},
{"business_field", request.BusinessField},
{"default_currency_id", request.DefaultCurrencyId},
{"include_sample_data", request.IncludeSampleData}
}
Dim root = Await SendJsonAsync(HttpMethod.Post, "api/v1/businesses", payload, includeAuth:=True, ct:=ct).ConfigureAwait(False)
Return ParseBusiness(GetDataToken(root))
End Function
Public Async Function GetBusinessDetailsAsync(businessId As Integer, Optional ct As CancellationToken = Nothing) As Task(Of HesabixBusiness)
Dim root = Await SendJsonAsync(HttpMethod.Post, "api/v1/businesses/" & businessId.ToString() & "/details", New JObject(), includeAuth:=True, ct:=ct).ConfigureAwait(False)
Return ParseBusiness(GetDataToken(root))
End Function
Public Async Function CreatePersonAsync(businessId As Integer, payload As JObject, Optional ct As CancellationToken = Nothing) As Task(Of Integer)
Dim root = Await SendJsonAsync(HttpMethod.Post, "api/v1/persons/businesses/" & businessId.ToString() & "/persons/create", payload, includeAuth:=True, ct:=ct).ConfigureAwait(False)
Return GetInt(GetDataToken(root), "id")
End Function
''' <summary>
''' ایجاد/به‌روزرسانی گروهی اشخاص. هر آیتم: { client_ref?, person_id?, payload }.
''' حداکثر ۱۰۰۰ آیتم در هر درخواست (سمت سرور).
''' </summary>
Public Async Function BulkUpsertPersonsAsync(
businessId As Integer,
items As JArray,
Optional createIfUpdateMissing As Boolean = True,
Optional ct As CancellationToken = Nothing
) As Task(Of BulkUpsertResult)
Dim body As New JObject From {
{"items", items},
{"create_if_update_missing", createIfUpdateMissing}
}
Dim root = Await SendJsonAsync(
HttpMethod.Post,
"api/v1/persons/businesses/" & businessId.ToString() & "/persons/bulk-upsert",
body,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Return ParseBulkUpsertResult(GetDataToken(root), "person_id")
End Function
Public Async Function CreateWarehouseAsync(businessId As Integer, payload As JObject, Optional ct As CancellationToken = Nothing) As Task(Of Integer)
Dim root = Await SendJsonAsync(HttpMethod.Post, "api/v1/warehouses/business/" & businessId.ToString(), payload, includeAuth:=True, ct:=ct).ConfigureAwait(False)
Dim data = GetDataToken(root)
Dim id = GetInt(data, "id")
If id = 0 AndAlso data("item") IsNot Nothing Then id = GetInt(DirectCast(data("item"), JObject), "id")
Return id
End Function
Public Async Function CreateBankAccountAsync(businessId As Integer, payload As JObject, Optional ct As CancellationToken = Nothing) As Task(Of Integer)
Dim root = Await SendJsonAsync(HttpMethod.Post, "api/v1/bank-accounts/businesses/" & businessId.ToString() & "/bank-accounts/create", payload, includeAuth:=True, ct:=ct).ConfigureAwait(False)
Return GetInt(GetDataToken(root), "id")
End Function
Public Async Function CreateCashRegisterAsync(businessId As Integer, payload As JObject, Optional ct As CancellationToken = Nothing) As Task(Of Integer)
Dim root = Await SendJsonAsync(HttpMethod.Post, "api/v1/cash-registers/businesses/" & businessId.ToString() & "/cash-registers/create", payload, includeAuth:=True, ct:=ct).ConfigureAwait(False)
Return GetInt(GetDataToken(root), "id")
End Function
Public Async Function CreatePettyCashAsync(businessId As Integer, payload As JObject, Optional ct As CancellationToken = Nothing) As Task(Of Integer)
Dim root = Await SendJsonAsync(HttpMethod.Post, "api/v1/petty-cash/businesses/" & businessId.ToString() & "/petty-cash/create", payload, includeAuth:=True, ct:=ct).ConfigureAwait(False)
Return GetInt(GetDataToken(root), "id")
End Function
Public Async Function CreateProductAsync(businessId As Integer, payload As JObject, Optional ct As CancellationToken = Nothing) As Task(Of Integer)
Dim root = Await SendJsonAsync(HttpMethod.Post, "api/v1/products/business/" & businessId.ToString(), payload, includeAuth:=True, ct:=ct).ConfigureAwait(False)
Return GetInt(GetDataToken(root), "id")
End Function
''' <summary>
''' ایجاد/به‌روزرسانی گروهی کالا. هر آیتم: { client_ref?, product_id?, payload }.
''' حداکثر ۱۰۰۰ آیتم در هر درخواست (سمت سرور).
''' </summary>
Public Async Function BulkUpsertProductsAsync(
businessId As Integer,
items As JArray,
Optional createIfUpdateMissing As Boolean = True,
Optional ct As CancellationToken = Nothing
) As Task(Of BulkUpsertResult)
Dim body As New JObject From {
{"items", items},
{"create_if_update_missing", createIfUpdateMissing}
}
Dim root = Await SendJsonAsync(
HttpMethod.Post,
"api/v1/products/business/" & businessId.ToString() & "/bulk-upsert",
body,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Return ParseBulkUpsertResult(GetDataToken(root), "product_id")
End Function
Public Async Function ListWarehousesAsync(businessId As Integer, Optional ct As CancellationToken = Nothing) As Task(Of Dictionary(Of String, Integer))
Dim root = Await GetJsonAsync("api/v1/warehouses/business/" & businessId.ToString(), ct).ConfigureAwait(False)
Dim map As New Dictionary(Of String, Integer)(StringComparer.OrdinalIgnoreCase)
For Each itemToken In GetItemsArray(GetDataToken(root))
If itemToken Is Nothing OrElse itemToken.Type <> JTokenType.Object Then Continue For
Dim item = DirectCast(itemToken, JObject)
Dim code = GetString(item, "code")
Dim id = GetInt(item, "id")
If Not String.IsNullOrWhiteSpace(code) AndAlso id > 0 AndAlso Not map.ContainsKey(code.Trim()) Then
map(code.Trim()) = id
End If
Next
Return map
End Function
Public Async Function FindPersonIdByCodeAsync(businessId As Integer, code As Integer, Optional ct As CancellationToken = Nothing) As Task(Of Integer)
Dim payload As New JObject From {
{"search", code.ToString()},
{"search_fields", New JArray From {"code"}},
{"take", 50},
{"skip", 0}
}
Dim root = Await SendJsonAsync(HttpMethod.Post, "api/v1/persons/businesses/" & businessId.ToString() & "/persons", payload, includeAuth:=True, ct:=ct).ConfigureAwait(False)
For Each itemToken In GetItemsArray(GetDataToken(root))
If itemToken Is Nothing OrElse itemToken.Type <> JTokenType.Object Then Continue For
Dim item = DirectCast(itemToken, JObject)
If GetInt(item, "code") = code Then
Return GetInt(item, "id")
End If
Next
Return 0
End Function
Public Async Function FindProductIdByCodeAsync(businessId As Integer, code As String, Optional ct As CancellationToken = Nothing) As Task(Of Integer)
If String.IsNullOrWhiteSpace(code) Then Return 0
Dim payload As New JObject From {
{"search", code.Trim()},
{"search_fields", New JArray From {"code"}},
{"take", 50},
{"skip", 0}
}
Dim root = Await SendJsonAsync(
HttpMethod.Post,
"api/v1/products/business/" & businessId.ToString() & "/search",
payload,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Dim codeNorm = code.Trim()
For Each itemToken In GetItemsArray(GetDataToken(root))
If itemToken Is Nothing OrElse itemToken.Type <> JTokenType.Object Then Continue For
Dim item = DirectCast(itemToken, JObject)
Dim itemCode = GetString(item, "code")
If Not String.IsNullOrWhiteSpace(itemCode) AndAlso
String.Equals(itemCode.Trim(), codeNorm, StringComparison.OrdinalIgnoreCase) Then
Return GetInt(item, "id")
End If
Next
Return 0
End Function
Public Async Function HasCurrentFiscalYearAsync(businessId As Integer, Optional ct As CancellationToken = Nothing) As Task(Of Boolean)
Try
Dim root = Await GetJsonAsync("api/v1/business/" & businessId.ToString() & "/fiscal-years", ct).ConfigureAwait(False)
Dim items = GetItemsArray(GetDataToken(root))
Return items IsNot Nothing AndAlso items.Count > 0
Catch
Return False
End Try
End Function
Public Async Function ListFiscalYearsAsync(businessId As Integer, Optional ct As CancellationToken = Nothing) As Task(Of List(Of HesabixFiscalYear))
Dim root = Await GetJsonAsync("api/v1/business/" & businessId.ToString() & "/fiscal-years", ct).ConfigureAwait(False)
Dim list As New List(Of HesabixFiscalYear)
For Each itemToken In GetItemsArray(GetDataToken(root))
Dim obj = TryCast(itemToken, JObject)
If obj Is Nothing Then Continue For
list.Add(ParseFiscalYear(obj))
Next
Return list
End Function
Public Async Function UpdateCurrentFiscalYearAsync(
businessId As Integer,
title As String,
startDate As Date,
endDate As Date,
Optional ct As CancellationToken = Nothing
) As Task(Of HesabixFiscalYear)
Dim body As New JObject From {
{"title", title},
{"start_date", ApiDateFormat.ToIsoDate(startDate)},
{"end_date", ApiDateFormat.ToIsoDate(endDate)}
}
Dim root = Await SendJsonAsync(
HttpMethod.Put,
"api/v1/business/" & businessId.ToString() & "/fiscal-years/current",
body,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Dim data = GetDataToken(root)
Return ParseFiscalYear(data)
End Function
Public Async Function EnsureFiscalYearsAsync(
businessId As Integer,
years As IEnumerable(Of HolooFiscalYearSlice),
Optional currentStartDate As Date? = Nothing,
Optional ct As CancellationToken = Nothing
) As Task(Of EnsureFiscalYearsResult)
Dim arr As New JArray()
For Each y In years
arr.Add(New JObject From {
{"title", y.Title},
{"start_date", ApiDateFormat.ToIsoDate(y.StartDate)},
{"end_date", ApiDateFormat.ToIsoDate(y.EndDate)}
})
Next
Dim body As New JObject From {{"years", arr}}
If currentStartDate.HasValue Then
body("current_start_date") = ApiDateFormat.ToIsoDate(currentStartDate.Value)
End If
Dim root = Await SendJsonAsync(
HttpMethod.Post,
"api/v1/business/" & businessId.ToString() & "/fiscal-years/migration/ensure",
body,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Dim data = GetDataToken(root)
Dim result As New EnsureFiscalYearsResult With {
.CreatedCount = GetInt(data, "created_count"),
.ReusedCount = GetInt(data, "reused_count")
}
Dim currentTok = data("current")
If currentTok IsNot Nothing AndAlso currentTok.Type = JTokenType.Object Then
result.Current = ParseFiscalYear(DirectCast(currentTok, JObject))
End If
For Each itemToken In GetItemsArray(data)
Dim obj = TryCast(itemToken, JObject)
If obj Is Nothing Then Continue For
result.Items.Add(ParseFiscalYear(obj))
Next
' برخی پاسخ‌ها items را مستقیم در data.items دارند؛ GetItemsArray همان را می‌خواند.
If result.Items.Count = 0 Then
Dim itemsTok = data("items")
If itemsTok IsNot Nothing AndAlso itemsTok.Type = JTokenType.Array Then
For Each itemToken In DirectCast(itemsTok, JArray)
Dim obj = TryCast(itemToken, JObject)
If obj Is Nothing Then Continue For
result.Items.Add(ParseFiscalYear(obj))
Next
End If
End If
Return result
End Function
Public Async Function SetCurrentFiscalYearAsync(businessId As Integer, fiscalYearId As Integer, Optional ct As CancellationToken = Nothing) As Task(Of HesabixFiscalYear)
' مهم: از مسیر .../fiscal-years/{id}/set-current استفاده می‌کنیم.
' مسیر .../migration/set-current با پارامتر {fiscal_year_id} تداخل داشت و "migration" به‌عنوان int پارس می‌شد.
Dim root = Await SendJsonAsync(
HttpMethod.Post,
"api/v1/business/" & businessId.ToString() & "/fiscal-years/" & fiscalYearId.ToString() & "/set-current",
New JObject(),
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Dim data = GetDataToken(root)
Return ParseFiscalYear(data)
End Function
Public Async Function BulkUpsertInvoicesAsync(
businessId As Integer,
items As JArray,
Optional ct As CancellationToken = Nothing
) As Task(Of BulkUpsertResult)
Dim body As New JObject From {
{"items", items},
{"migration_mode", True}
}
Dim root = Await SendJsonAsync(
HttpMethod.Post,
"api/v1/invoices/business/" & businessId.ToString() & "/bulk-upsert",
body,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Return ParseBulkUpsertResult(GetDataToken(root), "invoice_id")
End Function
Public Async Function BulkUpsertReceiptsPaymentsAsync(
businessId As Integer,
items As JArray,
Optional migrationMode As Boolean = True,
Optional ct As CancellationToken = Nothing
) As Task(Of BulkUpsertResult)
Dim body As New JObject From {
{"items", items},
{"migration_mode", migrationMode}
}
Dim root = Await SendJsonAsync(
HttpMethod.Post,
"api/v1/businesses/" & businessId.ToString() & "/receipts-payments/bulk-upsert",
body,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Return ParseBulkUpsertResult(GetDataToken(root), "document_id")
End Function
Public Async Function BulkUpsertExpenseIncomeAsync(
businessId As Integer,
items As JArray,
Optional migrationMode As Boolean = True,
Optional ct As CancellationToken = Nothing
) As Task(Of BulkUpsertResult)
Dim body As New JObject From {
{"items", items},
{"migration_mode", migrationMode}
}
Dim root = Await SendJsonAsync(
HttpMethod.Post,
"api/v1/businesses/" & businessId.ToString() & "/expense-income/bulk-upsert",
body,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Return ParseBulkUpsertResult(GetDataToken(root), "document_id")
End Function
Public Async Function CreateCheckAsync(businessId As Integer, payload As JObject, Optional ct As CancellationToken = Nothing) As Task(Of Integer)
Dim root = Await SendJsonAsync(
HttpMethod.Post,
"api/v1/checks/businesses/" & businessId.ToString() & "/checks/create",
payload,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Return GetInt(GetDataToken(root), "id")
End Function
Public Async Function CreateManualDocumentAsync(businessId As Integer, payload As JObject, Optional ct As CancellationToken = Nothing) As Task(Of Integer)
Dim root = Await SendJsonAsync(
HttpMethod.Post,
"api/v1/businesses/" & businessId.ToString() & "/documents/manual",
payload,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Return GetInt(GetDataToken(root), "id")
End Function
Public Async Function ClearCheckAsync(checkId As Integer, bankAccountId As Integer, Optional documentDate As Date? = Nothing, Optional ct As CancellationToken = Nothing) As Task
Dim body As New JObject From {{"bank_account_id", bankAccountId}}
If documentDate.HasValue Then body("document_date") = ApiDateFormat.ToIsoDate(documentDate.Value)
Await SendJsonAsync(
HttpMethod.Post,
"api/v1/checks/checks/" & checkId.ToString() & "/actions/clear",
body,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
End Function
Public Async Function DepositCheckAsync(checkId As Integer, bankAccountId As Integer, Optional documentDate As Date? = Nothing, Optional ct As CancellationToken = Nothing) As Task
Dim body As New JObject From {{"bank_account_id", bankAccountId}}
If documentDate.HasValue Then body("document_date") = ApiDateFormat.ToIsoDate(documentDate.Value)
Await SendJsonAsync(
HttpMethod.Post,
"api/v1/checks/checks/" & checkId.ToString() & "/actions/deposit",
body,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
End Function
Public Async Function ReturnCheckAsync(
checkId As Integer,
Optional returnType As String = "to_drawer",
Optional documentDate As Date? = Nothing,
Optional ct As CancellationToken = Nothing
) As Task
Dim body As New JObject From {{"return_type", returnType}}
If documentDate.HasValue Then body("document_date") = ApiDateFormat.ToIsoDate(documentDate.Value)
Await SendJsonAsync(
HttpMethod.Post,
"api/v1/checks/checks/" & checkId.ToString() & "/actions/return",
body,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
End Function
Public Async Function ListAccountCodeMapAsync(businessId As Integer, Optional ct As CancellationToken = Nothing) As Task(Of Dictionary(Of String, Integer))
Dim root = Await GetJsonAsync("api/v1/accounts/business/" & businessId.ToString(), ct).ConfigureAwait(False)
Dim map As New Dictionary(Of String, Integer)(StringComparer.OrdinalIgnoreCase)
For Each itemToken In GetItemsArray(GetDataToken(root))
Dim obj = TryCast(itemToken, JObject)
If obj Is Nothing Then Continue For
Dim code = GetString(obj, "code")
Dim id = GetInt(obj, "id")
If Not String.IsNullOrWhiteSpace(code) AndAlso id > 0 AndAlso Not map.ContainsKey(code.Trim()) Then
map(code.Trim()) = id
End If
Next
Return map
End Function
''' <summary>ایجاد حساب اختصاصی کسب‌وکار. parent_id باید والد غیربرگ باشد.</summary>
Public Async Function CreateBusinessAccountAsync(businessId As Integer, payload As JObject, Optional ct As CancellationToken = Nothing) As Task(Of Integer)
Dim root = Await SendJsonAsync(
HttpMethod.Post,
"api/v1/accounts/business/" & businessId.ToString() & "/create",
payload,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Return GetInt(GetDataToken(root), "id")
End Function
Public Async Function UpsertOpeningBalanceAsync(businessId As Integer, payload As JObject, Optional ct As CancellationToken = Nothing) As Task(Of JObject)
Dim root = Await SendJsonAsync(
HttpMethod.Put,
"api/v1/businesses/" & businessId.ToString() & "/opening-balance",
payload,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Return GetDataToken(root)
End Function
Public Async Function PostOpeningBalanceAsync(businessId As Integer, fiscalYearId As Integer, Optional ct As CancellationToken = Nothing) As Task
Await SendJsonAsync(
HttpMethod.Post,
"api/v1/businesses/" & businessId.ToString() & "/opening-balance/post?fiscal_year_id=" & fiscalYearId.ToString(),
New JObject(),
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
End Function
Public Async Function FindAccountIdByCodeAsync(businessId As Integer, code As String, Optional ct As CancellationToken = Nothing) As Task(Of Integer)
If String.IsNullOrWhiteSpace(code) Then Return 0
Dim root = Await GetJsonAsync("api/v1/accounts/business/" & businessId.ToString(), ct).ConfigureAwait(False)
Dim data = GetDataToken(root)
Dim items = GetItemsArray(data)
If items.Count = 0 Then
Dim dataTok = root("data")
If dataTok IsNot Nothing AndAlso dataTok.Type = JTokenType.Array Then
items = DirectCast(dataTok, JArray)
End If
End If
Dim codeNorm = code.Trim()
For Each itemToken In items
Dim obj = TryCast(itemToken, JObject)
If obj Is Nothing Then Continue For
Dim itemCode = GetString(obj, "code")
If String.Equals(If(itemCode, "").Trim(), codeNorm, StringComparison.OrdinalIgnoreCase) Then
Return GetInt(obj, "id")
End If
Next
Return 0
End Function
Public Async Function BulkWarehouseOperationsAsync(
businessId As Integer,
invoiceIds As IEnumerable(Of Integer),
operation As String,
Optional ct As CancellationToken = Nothing
) As Task(Of JObject)
Dim ids As New JArray()
For Each id In invoiceIds
ids.Add(id)
Next
Dim body As New JObject From {
{"invoice_ids", ids},
{"operation", operation}
}
Dim root = Await SendJsonAsync(
HttpMethod.Post,
"api/v1/warehouse-docs/business/" & businessId.ToString() & "/invoices/bulk-warehouse-operations",
body,
includeAuth:=True,
ct:=ct
).ConfigureAwait(False)
Return GetDataToken(root)
End Function
Private Shared Function ParseFiscalYear(obj As JObject) As HesabixFiscalYear
' start_date ممکن است جلالی فرمت‌شده باشد؛ برای پارس ماشینی start_date_raw ارجح است
Dim startRaw = GetString(obj, "start_date_raw")
Dim endRaw = GetString(obj, "end_date_raw")
Return New HesabixFiscalYear With {
.Id = GetInt(obj, "id"),
.Title = GetString(obj, "title"),
.StartDate = If(Not String.IsNullOrWhiteSpace(startRaw), startRaw, GetString(obj, "start_date")),
.EndDate = If(Not String.IsNullOrWhiteSpace(endRaw), endRaw, GetString(obj, "end_date")),
.IsCurrent = GetBool(obj, "is_current", False)
}
End Function
Private Shared Function ParseBusiness(data As JObject) As HesabixBusiness
Dim biz As New HesabixBusiness With {
.Id = GetInt(data, "id"),
.Name = GetString(data, "name"),
.BusinessType = GetString(data, "business_type"),
.BusinessField = GetString(data, "business_field"),
.OwnerId = GetNullableInt(data, "owner_id"),
.CreatedAt = GetString(data, "created_at"),
.DefaultCurrencyId = GetInt(data, "default_currency_id")
}
Dim currencyToken = data("default_currency")
If currencyToken IsNot Nothing AndAlso currencyToken.Type = JTokenType.Object Then
Dim currencyObj = DirectCast(currencyToken, JObject)
If biz.DefaultCurrencyId = 0 Then biz.DefaultCurrencyId = GetInt(currencyObj, "id")
biz.DefaultCurrencyCode = GetString(currencyObj, "code")
biz.DefaultCurrencyTitle = GetString(currencyObj, "title")
If String.IsNullOrWhiteSpace(biz.DefaultCurrencyTitle) Then biz.DefaultCurrencyTitle = GetString(currencyObj, "name")
End If
Return biz
End Function
Private Shared Function ParseUser(data As JObject) As HesabixUser
Return New HesabixUser With {
.Id = GetInt(data, "id"),
.Email = GetString(data, "email"),
.Mobile = GetString(data, "mobile"),
.FirstName = GetString(data, "first_name"),
.LastName = GetString(data, "last_name"),
.IsActive = GetBool(data, "is_active", True)
}
End Function
Private Shared Function ParseCurrency(item As JObject) As CurrencyInfo
Return New CurrencyInfo With {
.Id = GetInt(item, "id"),
.Name = GetString(item, "name"),
.Title = GetString(item, "title"),
.Symbol = GetString(item, "symbol"),
.Code = GetString(item, "code")
}
End Function
Private Function GetBaseUrlSnapshot() As String
SyncLock _sync
Return _baseUrl
End SyncLock
End Function
Private Function GetApiKeySnapshot() As String
SyncLock _sync
Return _apiKey
End SyncLock
End Function
Private Function BuildRequestUri(relativeUrl As String) As Uri
Dim baseUrl = GetBaseUrlSnapshot()
If String.IsNullOrWhiteSpace(baseUrl) Then
Throw New HesabixApiException("ابتدا آدرس سرور حسابیکس را وارد کنید.")
End If
Dim path = relativeUrl.TrimStart("/"c)
Return New Uri(baseUrl & "/" & path)
End Function
Private Async Function GetJsonAsync(relativeUrl As String, ct As CancellationToken) As Task(Of JObject)
Return Await SendJsonAsync(HttpMethod.Get, relativeUrl, Nothing, includeAuth:=True, ct:=ct).ConfigureAwait(False)
End Function
Private Async Function SendJsonAsync(method As HttpMethod, relativeUrl As String, payload As JToken, includeAuth As Boolean, ct As CancellationToken) As Task(Of JObject)
Using request As New HttpRequestMessage(method, BuildRequestUri(relativeUrl))
If includeAuth Then
Dim key = GetApiKeySnapshot()
If Not String.IsNullOrWhiteSpace(key) Then
request.Headers.TryAddWithoutValidation("Authorization", "ApiKey " & key)
End If
End If
If payload IsNot Nothing Then
request.Content = New StringContent(payload.ToString(Formatting.None), Encoding.UTF8, "application/json")
End If
Dim response As HttpResponseMessage = Nothing
Try
response = Await _http.SendAsync(request, HttpCompletionOption.ResponseContentRead, ct).ConfigureAwait(False)
Catch ex As OperationCanceledException
Throw
Catch ex As Exception
Throw New HesabixApiException("اتصال به سرور حسابیکس برقرار نشد. آدرس سرور و اینترنت را بررسی کنید.", inner:=ex)
End Try
Using response
Dim body = Await response.Content.ReadAsStringAsync().ConfigureAwait(False)
Dim root As JObject
Try
If String.IsNullOrWhiteSpace(body) Then
root = New JObject()
Else
root = JObject.Parse(body)
End If
Catch ex As Exception
Throw New HesabixApiException("پاسخ نامعتبر از سرور (کد " & CInt(response.StatusCode).ToString() & ").", CInt(response.StatusCode), inner:=ex)
End Try
Dim success = True
Dim successToken = root("success")
If successToken IsNot Nothing AndAlso successToken.Type = JTokenType.Boolean Then
success = successToken.Value(Of Boolean)()
End If
If Not response.IsSuccessStatusCode OrElse Not success Then
Dim message As String = Nothing
Dim errorCode As String = Nothing
Dim errToken = root("error")
If errToken IsNot Nothing AndAlso errToken.Type = JTokenType.Object Then
Dim errObj = DirectCast(errToken, JObject)
message = GetString(errObj, "message")
errorCode = GetString(errObj, "code")
Dim details = errObj("details")
If details IsNot Nothing AndAlso details.Type = JTokenType.Array AndAlso details.HasValues Then
Dim first = details(0)
If first IsNot Nothing AndAlso first.Type = JTokenType.Object Then
Dim detailMsg = GetString(DirectCast(first, JObject), "msg")
If Not String.IsNullOrWhiteSpace(detailMsg) Then
If String.IsNullOrWhiteSpace(message) Then
message = detailMsg
Else
message = message & " — " & detailMsg
End If
End If
End If
End If
End If
If String.IsNullOrWhiteSpace(message) Then message = GetString(root, "message")
If String.IsNullOrWhiteSpace(errorCode) Then errorCode = GetString(root, "error_code")
If String.IsNullOrWhiteSpace(message) Then
message = "خطای سرور (کد " & CInt(response.StatusCode).ToString() & ")"
End If
Throw New HesabixApiException(message, CInt(response.StatusCode), errorCode)
End If
Return root
End Using
End Using
End Function
Private Shared Function GetDataToken(root As JObject) As JObject
Dim data = root("data")
If data IsNot Nothing AndAlso data.Type = JTokenType.Object Then
Return DirectCast(data, JObject)
End If
If data IsNot Nothing AndAlso data.Type = JTokenType.Array Then
Return New JObject From {{"items", data}}
End If
Return root
End Function
Private Shared Function GetItemsArray(data As JObject) As JArray
Dim items = data("items")
If items IsNot Nothing AndAlso items.Type = JTokenType.Array Then
Return DirectCast(items, JArray)
End If
Return New JArray()
End Function
Private Shared Function ParseBulkUpsertResult(data As JObject, entityIdField As String) As BulkUpsertResult
Dim result As New BulkUpsertResult()
Dim resultsToken = If(data Is Nothing, Nothing, data("results"))
If resultsToken IsNot Nothing AndAlso resultsToken.Type = JTokenType.Array Then
For Each itemToken As JToken In DirectCast(resultsToken, JArray)
If itemToken Is Nothing OrElse itemToken.Type <> JTokenType.Object Then Continue For
Dim item = DirectCast(itemToken, JObject)
result.Results.Add(New BulkUpsertItemResult With {
.Index = GetInt(item, "index"),
.ClientRef = GetString(item, "client_ref"),
.Status = GetString(item, "status"),
.EntityId = GetInt(item, entityIdField),
.ErrorCode = GetString(item, "error_code"),
.Message = GetString(item, "message")
})
Next
End If
Dim summary = If(data Is Nothing, Nothing, data("summary"))
If summary IsNot Nothing AndAlso summary.Type = JTokenType.Object Then
Dim s = DirectCast(summary, JObject)
result.Total = GetInt(s, "total")
result.Created = GetInt(s, "created")
result.Updated = GetInt(s, "updated")
result.Failed = GetInt(s, "failed")
Else
result.Total = result.Results.Count
For Each r In result.Results
If String.Equals(r.Status, "created", StringComparison.OrdinalIgnoreCase) Then
result.Created += 1
ElseIf String.Equals(r.Status, "updated", StringComparison.OrdinalIgnoreCase) Then
result.Updated += 1
ElseIf String.Equals(r.Status, "failed", StringComparison.OrdinalIgnoreCase) Then
result.Failed += 1
End If
Next
End If
Return result
End Function
Private Shared Function GetString(token As JToken, name As String) As String
If token Is Nothing Then Return Nothing
Dim prop = token(name)
If prop Is Nothing OrElse prop.Type = JTokenType.Null Then Return Nothing
Return prop.ToString()
End Function
Private Shared Function GetInt(token As JToken, name As String) As Integer
Dim prop = If(token Is Nothing, Nothing, token(name))
If prop Is Nothing OrElse prop.Type = JTokenType.Null Then Return 0
Dim value As Integer
If Integer.TryParse(prop.ToString(), value) Then Return value
Return 0
End Function
Private Shared Function GetNullableInt(token As JToken, name As String) As Integer?
Dim prop = If(token Is Nothing, Nothing, token(name))
If prop Is Nothing OrElse prop.Type = JTokenType.Null Then Return Nothing
Dim value As Integer
If Integer.TryParse(prop.ToString(), value) Then Return value
Return Nothing
End Function
Private Shared Function GetBool(token As JToken, name As String, Optional defaultValue As Boolean = False) As Boolean
Dim prop = If(token Is Nothing, Nothing, token(name))
If prop Is Nothing OrElse prop.Type = JTokenType.Null Then Return defaultValue
Dim value As Boolean
If Boolean.TryParse(prop.ToString(), value) Then Return value
Return defaultValue
End Function
Public Sub Dispose() Implements IDisposable.Dispose
If _disposed Then Return
_disposed = True
_http.Dispose()
End Sub
End Class