Compare commits

...

2 Commits

16 changed files with 1228 additions and 0 deletions

View File

@ -0,0 +1,108 @@
namespace ActiveAllocator.API.Controllers;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using ActiveAllocator.API.Models.Accounts;
using ActiveAllocator.API.Services;
using ActiveAllocator.API.Config;
using System.Collections.Generic;
using ActiveAllocator.API.Entities;
using Microsoft.AspNetCore.Authorization;
using ActiveAllocator.API.Models.Integrations;
[Authorize]
[ApiController]
[Route("[controller]")]
public class IntegrationController : ControllerBase
{
private IIntegrationService _integrationService;
private IMapper _mapper;
private readonly AppSettings _appSettings;
public IntegrationController(
IIntegrationService integrationService,
IMapper mapper,
IOptions<AppSettings> appSettings)
{
_integrationService = integrationService;
_mapper = mapper;
_appSettings = appSettings.Value;
}
[HttpGet]
public IActionResult GetAll()
{
List<IntegrationDTO> DTOs = new();
foreach (var item in _integrationService.GetAll())
DTOs.Add(_mapper.Map<Integration, IntegrationDTO>(item));
return Ok(DTOs);
}
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
return Ok(_mapper.Map<Integration, IntegrationDTO>(_integrationService.GetById(id)));
}
[HttpPost]
public IActionResult Create([FromBody]IntegrationCreateRequest model)
{
_integrationService.Create(model);
return Ok(new { message = "integration created" });
}
[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody]IntegrationUpdateRequest model)
{
_integrationService.Update(id, model);
return Ok(new { message = "integration updated" });
}
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
_integrationService.Delete(id);
return Ok(new { message = "integration deleted" });
}
[HttpGet("Accounts")]
public IActionResult GetAllAccounts(int? integration_id = null)
{
List<IntegrationAccountDTO> DTOs = new();
foreach (var item in _integrationService.GetAllAccounts(integration_id))
DTOs.Add(_mapper.Map<IntegrationAccount, IntegrationAccountDTO>(item));
return Ok(DTOs);
}
[HttpGet("Accounts/{account_id}")]
public IActionResult GetAccountById(int account_id)
{
return Ok(_mapper.Map<IntegrationAccount, IntegrationAccountDTO>(_integrationService.GetAccountById(account_id)));
}
[HttpPost("Accounts")]
public IActionResult CreateAccount([FromBody]IntegrationAccountCreateRequest model)
{
_integrationService.CreateAccount(model);
return Ok(new { message = "integration account created" });
}
[HttpPut("Accounts/{id}")]
public IActionResult UpdateAccount(int id, [FromBody]IntegrationAccountUpdateRequest model)
{
_integrationService.UpdateAccount(id, model);
return Ok(new { message = "integration account updated" });
}
[HttpDelete("Accounts/{id}")]
public IActionResult DeleteAccount(int id)
{
_integrationService.DeleteAccount(id);
return Ok(new { message = "integration account deleted" });
}
}

View File

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
using ActiveAllocator.API.Enums;
namespace ActiveAllocator.API.Entities;
public class Integration
{
public int Id { get; set; }
public string Name { get; set; }
public string Version { get; set; }
public string ApiUrl { get; set; }
public DateTime CreatedOn { get; set; }
public IntegrationType Type { get; set; }
}

View File

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
using ActiveAllocator.API.Enums;
namespace ActiveAllocator.API.Entities;
public class IntegrationAccount
{
public int Id { get; set; }
public string Name { get; set; }
public string ApiKey { get; set; }
public DateTime CreatedOn { get; set; }
public Integration Integration { get; set; }
public Account LinkedAccount { get; set; }
}

View File

@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace ActiveAllocator.API.Enums;
public enum IntegrationType
{
AccountImporter,
TransactionImporter,
TransactionMapper,
}

View File

@ -10,6 +10,7 @@ using ActiveAllocator.API.Services;
using System.Runtime.Serialization;
using ActiveAllocator.API.Models.Transactions;
using ActiveAllocator.API.Models.Autoclass;
using ActiveAllocator.API.Models.Integrations;
public class AutoMapperProfile : Profile
{
@ -207,5 +208,11 @@ public class AutoMapperProfile : Profile
CreateMap<AutoclassRule, AutoclassRuleDTO>()
.ForMember(dest => dest.AccountId, opt => opt.MapFrom(src => src.Account.Id));
// Envelope -> EnvelopeDTO
CreateMap<Integration, IntegrationDTO>();
CreateMap<IntegrationAccount, IntegrationAccountDTO>()
.ForMember(dest => dest.IntegrationId, opt => opt.MapFrom(src => src.Integration.Id))
.ForMember(dest => dest.LinkedAccountId, opt => opt.MapFrom(src => src.LinkedAccount.Id));
}
}

View File

@ -454,4 +454,6 @@ public class DataContext : DbContext
public DbSet<AutoclassRule> AutoclassRules { get; set; }
public DbSet<AutoclassExpression> AutoclassExpressions { get; set; }
public DbSet<AutoclassChange> AutoclassChanges { get; set; }
public DbSet<Integration> Integrations { get; set; }
public DbSet<IntegrationAccount> IntegrationAccounts { get; set; }
}

View File

@ -0,0 +1,606 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using ActiveAllocator.API.Config;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace ActiveAllocator.API.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20240405024649_Integrations")]
partial class Integrations
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("ActiveAllocator.API.Entities.Account", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("Balance")
.HasColumnType("numeric");
b.Property<DateTime>("CreatedOn")
.HasColumnType("timestamp with time zone");
b.Property<int>("CurrencyId")
.HasColumnType("integer");
b.Property<string>("ExternalAccountNumber")
.IsRequired()
.HasColumnType("text");
b.Property<decimal>("InitialBalance")
.HasColumnType("numeric");
b.Property<DateTime>("LastActivity")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("UserId")
.HasColumnType("integer");
b.Property<decimal>("VirtualBalance")
.HasColumnType("numeric");
b.HasKey("Id");
b.HasIndex("CurrencyId");
b.HasIndex("UserId");
b.ToTable("Accounts");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.AutoclassChange", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("AutoclassRuleId")
.HasColumnType("integer");
b.Property<int>("Field")
.HasColumnType("integer");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AutoclassRuleId");
b.ToTable("AutoclassChanges");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.AutoclassExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("AutoclassRuleId")
.HasColumnType("integer");
b.Property<int>("CompareOperator")
.HasColumnType("integer");
b.Property<int>("TransactionField")
.HasColumnType("integer");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AutoclassRuleId");
b.ToTable("AutoclassExpressions");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.AutoclassRule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("AccountId")
.HasColumnType("integer");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AccountId");
b.ToTable("AutoclassRules");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.CurrencyType", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Code")
.IsRequired()
.HasColumnType("text");
b.Property<int>("DecimalPlaces")
.HasColumnType("integer");
b.Property<string>("Symbol")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("CurrencyTypes");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.Envelope", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("AccountId")
.HasColumnType("integer");
b.Property<decimal>("Balance")
.HasColumnType("numeric");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<decimal>("InitialBalance")
.HasColumnType("numeric");
b.Property<bool>("IsPersistant")
.HasColumnType("boolean");
b.Property<DateTime?>("LastTriggeredOn")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<TimeSpan>("Period")
.HasColumnType("interval");
b.Property<int>("Priority")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("AccountId");
b.ToTable("Envelopes");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.EnvelopeFundingMethod", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("EnvelopeId")
.HasColumnType("integer");
b.Property<int>("Mode")
.HasColumnType("integer");
b.Property<int>("Order")
.HasColumnType("integer");
b.Property<int>("PeriodsToLookback")
.HasColumnType("integer");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("EnvelopeId");
b.ToTable("EnvelopeFundingMethods");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.Integration", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ApiUrl")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedOn")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<string>("Version")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Integrations");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.IntegrationAccount", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ApiKey")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedOn")
.HasColumnType("timestamp with time zone");
b.Property<int>("IntegrationId")
.HasColumnType("integer");
b.Property<int>("LinkedAccountId")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("IntegrationId");
b.HasIndex("LinkedAccountId");
b.ToTable("IntegrationAccounts");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.Operation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("AbsoluteValue")
.HasColumnType("numeric");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<int>("HistoricLookbackDepth")
.HasColumnType("integer");
b.Property<TimeSpan>("HistoricPeriod")
.HasColumnType("interval");
b.Property<int>("HistoricStatistic")
.HasColumnType("integer");
b.Property<int>("Mode")
.HasColumnType("integer");
b.Property<bool>("Negative")
.HasColumnType("boolean");
b.Property<int>("Order")
.HasColumnType("integer");
b.Property<decimal>("Percentage")
.HasColumnType("numeric");
b.HasKey("Id");
b.ToTable("Operations");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.Transaction", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<DateTime>("CreatedOn")
.HasColumnType("timestamp with time zone");
b.Property<int?>("CreditAccountId")
.HasColumnType("integer");
b.Property<int?>("CreditEnvelopeId")
.HasColumnType("integer");
b.Property<int>("CurrencyTypeId")
.HasColumnType("integer");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<int?>("DebitAccountId")
.HasColumnType("integer");
b.Property<int?>("DebitEnvelopeId")
.HasColumnType("integer");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ExternalId")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("IsEnvelopeFundingTransaction")
.HasColumnType("boolean");
b.Property<bool>("IsPending")
.HasColumnType("boolean");
b.Property<string>("Notes")
.IsRequired()
.HasColumnType("text");
b.Property<List<string>>("Tags")
.HasColumnType("text[]");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CreditAccountId");
b.HasIndex("CreditEnvelopeId");
b.HasIndex("CurrencyTypeId");
b.HasIndex("DebitAccountId");
b.HasIndex("DebitEnvelopeId");
b.ToTable("Transactions");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ApiKey")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("FirstName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Role")
.HasColumnType("integer");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.Account", b =>
{
b.HasOne("ActiveAllocator.API.Entities.CurrencyType", "Currency")
.WithMany()
.HasForeignKey("CurrencyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ActiveAllocator.API.Entities.User", null)
.WithMany("Accounts")
.HasForeignKey("UserId");
b.Navigation("Currency");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.AutoclassChange", b =>
{
b.HasOne("ActiveAllocator.API.Entities.AutoclassRule", null)
.WithMany("Changes")
.HasForeignKey("AutoclassRuleId");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.AutoclassExpression", b =>
{
b.HasOne("ActiveAllocator.API.Entities.AutoclassRule", null)
.WithMany("Expressions")
.HasForeignKey("AutoclassRuleId");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.AutoclassRule", b =>
{
b.HasOne("ActiveAllocator.API.Entities.Account", "Account")
.WithMany()
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Account");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.Envelope", b =>
{
b.HasOne("ActiveAllocator.API.Entities.Account", "Account")
.WithMany("Envelopes")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Account");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.EnvelopeFundingMethod", b =>
{
b.HasOne("ActiveAllocator.API.Entities.Envelope", "Envelope")
.WithMany("FundingMethods")
.HasForeignKey("EnvelopeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Envelope");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.IntegrationAccount", b =>
{
b.HasOne("ActiveAllocator.API.Entities.Integration", "Integration")
.WithMany()
.HasForeignKey("IntegrationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ActiveAllocator.API.Entities.Account", "LinkedAccount")
.WithMany()
.HasForeignKey("LinkedAccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Integration");
b.Navigation("LinkedAccount");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.Transaction", b =>
{
b.HasOne("ActiveAllocator.API.Entities.Account", "CreditAccount")
.WithMany()
.HasForeignKey("CreditAccountId");
b.HasOne("ActiveAllocator.API.Entities.Envelope", "CreditEnvelope")
.WithMany()
.HasForeignKey("CreditEnvelopeId");
b.HasOne("ActiveAllocator.API.Entities.CurrencyType", "CurrencyType")
.WithMany()
.HasForeignKey("CurrencyTypeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ActiveAllocator.API.Entities.Account", "DebitAccount")
.WithMany()
.HasForeignKey("DebitAccountId");
b.HasOne("ActiveAllocator.API.Entities.Envelope", "DebitEnvelope")
.WithMany()
.HasForeignKey("DebitEnvelopeId");
b.Navigation("CreditAccount");
b.Navigation("CreditEnvelope");
b.Navigation("CurrencyType");
b.Navigation("DebitAccount");
b.Navigation("DebitEnvelope");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.Account", b =>
{
b.Navigation("Envelopes");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.AutoclassRule", b =>
{
b.Navigation("Changes");
b.Navigation("Expressions");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.Envelope", b =>
{
b.Navigation("FundingMethods");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.User", b =>
{
b.Navigation("Accounts");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,82 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace ActiveAllocator.API.Migrations
{
/// <inheritdoc />
public partial class Integrations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Integrations",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
Version = table.Column<string>(type: "text", nullable: false),
ApiUrl = table.Column<string>(type: "text", nullable: false),
CreatedOn = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Type = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Integrations", x => x.Id);
});
migrationBuilder.CreateTable(
name: "IntegrationAccounts",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
ApiKey = table.Column<string>(type: "text", nullable: false),
CreatedOn = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
IntegrationId = table.Column<int>(type: "integer", nullable: false),
LinkedAccountId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_IntegrationAccounts", x => x.Id);
table.ForeignKey(
name: "FK_IntegrationAccounts_Accounts_LinkedAccountId",
column: x => x.LinkedAccountId,
principalTable: "Accounts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_IntegrationAccounts_Integrations_IntegrationId",
column: x => x.IntegrationId,
principalTable: "Integrations",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_IntegrationAccounts_IntegrationId",
table: "IntegrationAccounts",
column: "IntegrationId");
migrationBuilder.CreateIndex(
name: "IX_IntegrationAccounts_LinkedAccountId",
table: "IntegrationAccounts",
column: "LinkedAccountId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "IntegrationAccounts");
migrationBuilder.DropTable(
name: "Integrations");
}
}
}

View File

@ -245,6 +245,71 @@ namespace ActiveAllocator.API.Migrations
b.ToTable("EnvelopeFundingMethods");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.Integration", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ApiUrl")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedOn")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<string>("Version")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Integrations");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.IntegrationAccount", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ApiKey")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedOn")
.HasColumnType("timestamp with time zone");
b.Property<int>("IntegrationId")
.HasColumnType("integer");
b.Property<int>("LinkedAccountId")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("IntegrationId");
b.HasIndex("LinkedAccountId");
b.ToTable("IntegrationAccounts");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.Operation", b =>
{
b.Property<int>("Id")
@ -457,6 +522,25 @@ namespace ActiveAllocator.API.Migrations
b.Navigation("Envelope");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.IntegrationAccount", b =>
{
b.HasOne("ActiveAllocator.API.Entities.Integration", "Integration")
.WithMany()
.HasForeignKey("IntegrationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ActiveAllocator.API.Entities.Account", "LinkedAccount")
.WithMany()
.HasForeignKey("LinkedAccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Integration");
b.Navigation("LinkedAccount");
});
modelBuilder.Entity("ActiveAllocator.API.Entities.Transaction", b =>
{
b.HasOne("ActiveAllocator.API.Entities.Account", "CreditAccount")

View File

@ -0,0 +1,11 @@
namespace ActiveAllocator.API.Models.Integrations;
public class IntegrationAccountCreateRequest
{
public string Name { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string MFAKey { get; set; }
public int IntegrationId { get; set; }
public int LinkedAccountId { get; set; }
}

View File

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
using ActiveAllocator.API.Enums;
namespace ActiveAllocator.API.Models.Integrations;
public class IntegrationAccountDTO
{
public int Id { get; set; }
public string Name { get; set; }
public string ApiKey { get; set; }
public DateTime CreatedOn { get; set; }
public int IntegrationId { get; set; }
public int LinkedAccountId { get; set; }
}

View File

@ -0,0 +1,11 @@
namespace ActiveAllocator.API.Models.Integrations;
public class IntegrationAccountUpdateRequest
{
public string Name { get; set; } = null;
public string Username { get; set; } = null;
public string Password { get; set; } = null;
public string MFAKey { get; set; } = null;
//public int? IntegrationId { get; set; } = null;
public int? LinkedAccountId { get; set; } = null;
}

View File

@ -0,0 +1,14 @@
namespace ActiveAllocator.API.Models.Integrations;
using System.ComponentModel.DataAnnotations;
using System.Runtime.InteropServices;
using ActiveAllocator.API.Entities;
using ActiveAllocator.API.Enums;
public class IntegrationCreateRequest
{
public string Name { get; set; }
public string Version { get; set; }
public string ApiUrl { get; set; }
public IntegrationType Type { get; set; }
}

View File

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
using ActiveAllocator.API.Enums;
namespace ActiveAllocator.API.Models.Integrations;
public class IntegrationDTO
{
public int Id { get; set; }
public string Name { get; set; }
public string Version { get; set; }
public string ApiUrl { get; set; }
public DateTime CreatedOn { get; set; }
public IntegrationType Type { get; set; }
}

View File

@ -0,0 +1,13 @@
namespace ActiveAllocator.API.Models.Integrations;
using System.ComponentModel.DataAnnotations;
using ActiveAllocator.API.Entities;
using ActiveAllocator.API.Enums;
public class IntegrationUpdateRequest
{
public string Name { get; set; } = null;
public string Version { get; set; } = null;
public string ApiUrl { get; set; } = null;
public IntegrationType? Type { get; set; } = null;
}

View File

@ -0,0 +1,224 @@
namespace ActiveAllocator.API.Services;
using ActiveAllocator.API.Entities;
using ActiveAllocator.API.Config;
using ActiveAllocator.API.Models.Integrations;
using System.Collections.Generic;
using System.Linq;
using System;
using Microsoft.EntityFrameworkCore;
public interface IIntegrationService
{
IEnumerable<Integration> GetAll();
IEnumerable<IntegrationAccount> GetAllAccounts(int? integrationId = null);
Integration GetById(int integrationId);
IntegrationAccount GetAccountById(int accountId);
void Create(IntegrationCreateRequest model);
void CreateAccount(IntegrationAccountCreateRequest model);
void Update(int integrationId, IntegrationUpdateRequest model);
void UpdateAccount(int integrationAccountId, IntegrationAccountUpdateRequest model);
void Delete(int integrationId);
void DeleteAccount(int integrationAccountId);
}
public class IntegrationService : IIntegrationService
{
private DataContext _context;
public IntegrationService(
DataContext context)
{
_context = context;
}
public IEnumerable<Integration> GetAll()
{
return _context.Integrations;
}
public IEnumerable<IntegrationAccount> GetAllAccounts(int? integrationId = null)
{
if (integrationId.HasValue)
return _context.IntegrationAccounts
.Include(a => a.Integration)
.Include(a => a.LinkedAccount)
.Where(a => a.Integration.Id == integrationId.Value);
else
return _context.IntegrationAccounts
.Include(a => a.Integration)
.Include(a => a.LinkedAccount);
}
public Integration GetById(int integrationId)
{
return GetIntegration(integrationId);
}
public IntegrationAccount GetAccountById(int accountId)
{
return GetIntegrationAccount(accountId);
}
public void Create(IntegrationCreateRequest model)
{
if (_context.Integrations.Any(a => a.Name.ToUpper() == model.Name.ToUpper()))
throw new AppException("Integration with name '" + model.Name + "' already exists");
if (_context.Integrations.Any(a => a.ApiUrl.ToUpper() == model.ApiUrl.ToUpper()))
throw new AppException("Integration with API Url '" + model.ApiUrl + "' already exists");
Integration integration = new Integration {
Name = model.Name,
Version = model.Version,
ApiUrl = model.ApiUrl,
CreatedOn = DateTime.UtcNow,
Type = model.Type
};
_context.Integrations.Add(integration);
_context.SaveChanges();
}
public void CreateAccount(IntegrationAccountCreateRequest model)
{
// Validate model info
if (_context.IntegrationAccounts
.Include(a => a.Integration)
.Any(a => a.Name.ToUpper() == model.Name.ToUpper() && a.Integration.Id == model.IntegrationId))
throw new AppException($"IntegrationAccount with name '{model.Name}' already exists on integration '{model.IntegrationId}'");
Integration integration = _context.Integrations.Find(model.IntegrationId) ?? throw new AppException($"Integration with id '{model.IntegrationId}' not found");
Account account = _context.Accounts.Find(model.LinkedAccountId) ?? throw new AppException($"Account with id '{model.LinkedAccountId}' not found");
// Get API Key from integration
string apiKey = RegisterRemoteIntegrationAccount(model.Username, model.Password, model.MFAKey);
// Create entity and insert into database
IntegrationAccount integrationAccount = new IntegrationAccount {
Name = model.Name,
ApiKey = apiKey,
CreatedOn = DateTime.UtcNow,
Integration = integration,
LinkedAccount = account
};
_context.IntegrationAccounts.Add(integrationAccount);
_context.SaveChanges();
}
public void Update(int integrationId, IntegrationUpdateRequest model)
{
Integration integration = GetIntegration(integrationId);
// validate
if (model.Name != integration.Name && _context.Integrations.Any(x => x.Name == model.Name))
throw new AppException("Integration with the name '" + model.Name + "' already exists");
// validate
if (model.ApiUrl != integration.ApiUrl && _context.Integrations.Any(x => x.ApiUrl == model.ApiUrl))
throw new AppException("Integration with the API Url '" + model.ApiUrl + "' already exists");
// Name
if (!string.IsNullOrWhiteSpace(model.Name))
integration.Name = model.Name;
// Api Url
if (!string.IsNullOrWhiteSpace(model.ApiUrl))
integration.ApiUrl = model.ApiUrl;
// Version
if (!string.IsNullOrWhiteSpace(model.Version))
integration.Version = model.Version;
// Types
if (model.Type.HasValue)
{
integration.Type = model.Type.Value;
}
_context.Integrations.Update(integration);
_context.SaveChanges();
}
public void UpdateAccount(int integrationAccountId, IntegrationAccountUpdateRequest model)
{
IntegrationAccount integrationAccount = GetIntegrationAccount(integrationAccountId);
// Check if new name is valid
if (model.Name != integrationAccount.Name && _context.IntegrationAccounts
.Include(a => a.Integration)
.Any(a => a.Name.ToUpper() == model.Name.ToUpper() && a.Integration.Id == integrationAccount.Integration.Id))
throw new AppException($"IntegrationAccount with name '{model.Name}' already exists on integration '{integrationAccount.Integration.Id}'");
// Check if username, password, and MFAKey are set
// If so, delete integration user, and remake one, update API key
if (model.LinkedAccountId.HasValue)
{
Account account = _context.Accounts.Find(model.LinkedAccountId.Value) ?? throw new AppException($"Could not find the account specified by LinkedAccountId");
integrationAccount.LinkedAccount = account;
}
if (model.Username != null || model.Password != null || model.MFAKey != null)
{
if (model.Username == null || model.Password == null || model.MFAKey == null)
throw new AppException($"IntegrationAccount credential change requires all new credentials");
string apiKey = UpdateRemoteIntegrationAccount(integrationAccount.Id, model.Username, model.Password, model.MFAKey);
integrationAccount.ApiKey = apiKey;
}
_context.IntegrationAccounts.Update(integrationAccount);
_context.SaveChanges();
}
public void Delete(int integrationId)
{
var integration = GetIntegration(integrationId);
_context.Integrations.Remove(integration);
_context.SaveChanges();
}
public void DeleteAccount(int integrationAccountId)
{
var integrationAccount = _context.IntegrationAccounts.Find(integrationAccountId) ?? throw new AppException($"Could not find the integration account specified to delete");
DeleteRemoteIntegrationAccount(integrationAccount.Id);
_context.IntegrationAccounts.Remove(integrationAccount);
_context.SaveChanges();
}
// helper methods
private string UpdateRemoteIntegrationAccount(int integrationAccountId, string username, string password, string mfaKey)
{
throw new NotImplementedException();
}
private void DeleteRemoteIntegrationAccount(int integrationAccountId)
{
throw new NotImplementedException();
}
private string RegisterRemoteIntegrationAccount(string username, string password, string mfaKey)
{
throw new NotImplementedException();
}
private Integration GetIntegration(int id)
{
var integration = _context.Integrations.Find(id) ?? throw new KeyNotFoundException("Integration not found");
return integration;
}
private IntegrationAccount GetIntegrationAccount(int id)
{
return _context.IntegrationAccounts
.Include(a => a.Integration)
.Include(a => a.LinkedAccount)
.FirstOrDefault(a => a.Id == id)
?? throw new KeyNotFoundException("IntegrationAccount not found");
}
}