feat: initial project setup
- 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
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
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!);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user