- Add .NET 8 backend with Clean Architecture - Add React + Vite + TypeScript frontend - Implement authentication with JWT - Implement Azure Blob Storage client - Implement OCR integration - Implement supplier matching service - Implement voucher generation - Implement Fortnox provider - Add unit and integration tests - Add Docker Compose configuration
154 lines
5.1 KiB
C#
154 lines
5.1 KiB
C#
using InvoiceMaster.Application.DTOs;
|
|
using InvoiceMaster.Core.Entities;
|
|
using InvoiceMaster.Core.Interfaces;
|
|
using InvoiceMaster.Integrations.Accounting;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace InvoiceMaster.API.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/v1/accounting")]
|
|
[Authorize]
|
|
public class AccountingController : ControllerBase
|
|
{
|
|
private readonly IAccountingSystemFactory _factory;
|
|
private readonly IRepository<AccountingConnection> _connectionRepository;
|
|
private readonly IRepository<User> _userRepository;
|
|
|
|
public AccountingController(
|
|
IAccountingSystemFactory factory,
|
|
IRepository<AccountingConnection> connectionRepository,
|
|
IRepository<User> userRepository)
|
|
{
|
|
_factory = factory;
|
|
_connectionRepository = connectionRepository;
|
|
_userRepository = userRepository;
|
|
}
|
|
|
|
[HttpGet("providers")]
|
|
public IActionResult GetProviders()
|
|
{
|
|
var userId = GetUserId();
|
|
var connections = _connectionRepository.GetAllAsync().Result
|
|
.Where(c => c.UserId == userId && c.IsActive)
|
|
.ToList();
|
|
|
|
var providers = new[]
|
|
{
|
|
new ProviderInfoDto("fortnox", "Fortnox", "Swedish accounting software", true,
|
|
connections.Any(c => c.Provider == "fortnox")),
|
|
new ProviderInfoDto("visma", "Visma eAccounting", "Nordic accounting software", false, false),
|
|
new ProviderInfoDto("hogia", "Hogia Smart", "Swedish accounting software", false, false)
|
|
};
|
|
|
|
return Ok(new { success = true, data = new { providers } });
|
|
}
|
|
|
|
[HttpGet("{provider}/auth/url")]
|
|
public IActionResult GetAuthUrl(string provider)
|
|
{
|
|
var redirectUri = $"{Request.Scheme}://{Request.Host}/api/v1/accounting/{provider}/auth/callback";
|
|
|
|
var state = Guid.NewGuid().ToString("N");
|
|
|
|
string authUrl = provider.ToLower() switch
|
|
{
|
|
"fortnox" => $"https://apps.fortnox.se/oauth-v1/auth?client_id=&redirect_uri={Uri.EscapeDataString(redirectUri)}&scope=supplier+voucher+account&state={state}",
|
|
_ => throw new NotSupportedException($"Provider '{provider}' is not supported")
|
|
};
|
|
|
|
return Ok(new
|
|
{
|
|
success = true,
|
|
data = new { provider, authorizationUrl = authUrl, state }
|
|
});
|
|
}
|
|
|
|
[HttpGet("{provider}/auth/callback")]
|
|
public async Task<IActionResult> AuthCallback(string provider, [FromQuery] string code, [FromQuery] string state)
|
|
{
|
|
var accounting = _factory.Create(provider);
|
|
var result = await accounting.AuthenticateAsync(code);
|
|
|
|
if (!result.Success)
|
|
{
|
|
return BadRequest(new { success = false, error = result.ErrorMessage });
|
|
}
|
|
|
|
var userId = GetUserId();
|
|
var connection = AccountingConnection.Create(
|
|
userId,
|
|
provider,
|
|
result.AccessToken ?? string.Empty,
|
|
result.RefreshToken ?? string.Empty,
|
|
result.ExpiresAt ?? DateTime.UtcNow.AddHours(1),
|
|
result.Scope,
|
|
result.CompanyInfo?.Name,
|
|
result.CompanyInfo?.OrganisationNumber);
|
|
|
|
await _connectionRepository.AddAsync(connection);
|
|
|
|
return Ok(new
|
|
{
|
|
success = true,
|
|
data = new
|
|
{
|
|
provider,
|
|
connected = true,
|
|
companyName = result.CompanyInfo?.Name,
|
|
companyOrgNumber = result.CompanyInfo?.OrganisationNumber,
|
|
connectedAt = connection.CreatedAt
|
|
}
|
|
});
|
|
}
|
|
|
|
[HttpGet("connections")]
|
|
public async Task<IActionResult> GetConnections()
|
|
{
|
|
var userId = GetUserId();
|
|
var connections = (await _connectionRepository.GetAllAsync())
|
|
.Where(c => c.UserId == userId && c.IsActive)
|
|
.Select(c => new ConnectionDto(
|
|
c.Provider,
|
|
true,
|
|
c.CompanyName,
|
|
c.CompanyOrgNumber,
|
|
c.Scope?.Split(' ').ToList(),
|
|
c.ExpiresAt,
|
|
new ConnectionSettingsDto(
|
|
c.DefaultVoucherSeries,
|
|
c.DefaultAccountCode,
|
|
c.AutoAttachPdf,
|
|
c.AutoCreateSupplier)))
|
|
.ToList();
|
|
|
|
return Ok(new { success = true, data = new { connections } });
|
|
}
|
|
|
|
[HttpDelete("connections/{provider}")]
|
|
public async Task<IActionResult> Disconnect(string provider)
|
|
{
|
|
var userId = GetUserId();
|
|
var connections = await _connectionRepository.GetAllAsync();
|
|
var connection = connections.FirstOrDefault(c =>
|
|
c.UserId == userId &&
|
|
c.Provider.Equals(provider, StringComparison.OrdinalIgnoreCase) &&
|
|
c.IsActive);
|
|
|
|
if (connection == null)
|
|
{
|
|
return NotFound(new { success = false, error = "Connection not found" });
|
|
}
|
|
|
|
connection.Deactivate();
|
|
return Ok(new { success = true });
|
|
}
|
|
|
|
private Guid GetUserId()
|
|
{
|
|
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
|
return Guid.Parse(userId!);
|
|
}
|
|
}
|