This commit includes all previously untracked backend files: Domain: - Accounts, Attachments, BankConnections, Customers - FiscalYears, Invoices, JournalEntryDrafts - Orders, Products, UserAccess Commands & Handlers: - Full CQRS command structure for all domains Repositories: - PostgreSQL repositories for all read models - Bank transaction and ledger repositories GraphQL: - Input types, scalars, and types for all entities - Mutations and queries Infrastructure: - Banking integration (Enable Banking client) - File storage, Invoicing, Reporting, SAF-T export - Database migrations (003-029) Tests: - Integration tests for GraphQL endpoints - Domain tests - Invoicing and reporting tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
787 lines
28 KiB
C#
787 lines
28 KiB
C#
using Books.Api.EventFlow.Repositories;
|
|
using Books.Api.Tests.Helpers;
|
|
using Books.Api.Tests.Infrastructure;
|
|
using AwesomeAssertions;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace Books.Api.Tests.GraphQL;
|
|
|
|
/// <summary>
|
|
/// Integration tests for JournalEntryDraft (Kassekladde) GraphQL operations.
|
|
/// </summary>
|
|
[Trait("Category", "Integration")]
|
|
public class JournalEntryDraftGraphQLTests(TestWebApplicationFactory factory)
|
|
: IntegrationTestBase(factory)
|
|
{
|
|
#region Create Draft Tests
|
|
|
|
[Fact]
|
|
public async Task Mutation_CreateJournalEntryDraft_CreatesSuccessfully()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Draft Test Company");
|
|
|
|
// Act
|
|
var response = await graphqlClient.MutateAsync<CreateDraftResponse>("""
|
|
mutation CreateDraft($input: CreateJournalEntryDraftInput!) {
|
|
createJournalEntryDraft(input: $input) {
|
|
id
|
|
companyId
|
|
name
|
|
status
|
|
createdBy
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
name = "Januar Udgifter"
|
|
}
|
|
});
|
|
|
|
// Assert
|
|
response.EnsureNoErrors();
|
|
response.Data.Should().NotBeNull();
|
|
response.Data!.CreateJournalEntryDraft.Should().NotBeNull();
|
|
response.Data.CreateJournalEntryDraft!.Name.Should().Be("Januar Udgifter");
|
|
response.Data.CreateJournalEntryDraft.Status.Should().Be("active");
|
|
response.Data.CreateJournalEntryDraft.CompanyId.Should().Be(companyId);
|
|
response.Data.CreateJournalEntryDraft.Id.Should().StartWith("journalentrydraft-");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_CreateJournalEntryDraft_FailsWithoutCompanyHeader()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
// Note: We're NOT setting X-Company-Id header
|
|
|
|
// Act
|
|
var response = await graphqlClient.MutateAsync<CreateDraftResponse>("""
|
|
mutation CreateDraft($input: CreateJournalEntryDraftInput!) {
|
|
createJournalEntryDraft(input: $input) {
|
|
id
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId = "some-company-id",
|
|
name = "Test Draft"
|
|
}
|
|
});
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_CreateJournalEntryDraft_FailsWithEmptyName()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Empty Name Test Company");
|
|
|
|
// Act
|
|
var response = await graphqlClient.MutateAsync<CreateDraftResponse>("""
|
|
mutation CreateDraft($input: CreateJournalEntryDraftInput!) {
|
|
createJournalEntryDraft(input: $input) {
|
|
id
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
name = " " // Whitespace only
|
|
}
|
|
});
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorInfo = string.Join(" ", response.Errors!.Select(e =>
|
|
e.Message + " " + (e.Extensions?.GetValueOrDefault("details")?.ToString() ?? "")));
|
|
errorInfo.Should().Contain("Draft name is required");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Update Draft Tests
|
|
|
|
[Fact]
|
|
public async Task Mutation_UpdateJournalEntryDraft_UpdatesSuccessfully()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Update Draft Test Company");
|
|
var draftId = await CreateDraftAsync(graphqlClient, companyId, "Original Name");
|
|
|
|
// Wait for eventual consistency
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IJournalEntryDraftRepository>();
|
|
return await repo.GetByIdAsync(draftId);
|
|
});
|
|
|
|
// Act
|
|
var response = await graphqlClient.MutateAsync<UpdateDraftResponse>("""
|
|
mutation UpdateDraft($input: UpdateJournalEntryDraftInput!) {
|
|
updateJournalEntryDraft(input: $input) {
|
|
id
|
|
name
|
|
documentDate
|
|
description
|
|
lines {
|
|
lineNumber
|
|
accountId
|
|
debitAmount
|
|
creditAmount
|
|
description
|
|
}
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
id = draftId,
|
|
name = "Updated Name",
|
|
documentDate = "2025-01-15",
|
|
description = "Test beskrivelse",
|
|
lines = new[]
|
|
{
|
|
new { lineNumber = 1, accountId = (string?)null, debitAmount = 1000m, creditAmount = 0m, description = "Debet linje" },
|
|
new { lineNumber = 2, accountId = (string?)null, debitAmount = 0m, creditAmount = 1000m, description = "Kredit linje" }
|
|
}
|
|
}
|
|
});
|
|
|
|
// Assert
|
|
response.EnsureNoErrors();
|
|
response.Data!.UpdateJournalEntryDraft!.Name.Should().Be("Updated Name");
|
|
response.Data.UpdateJournalEntryDraft.Description.Should().Be("Test beskrivelse");
|
|
response.Data.UpdateJournalEntryDraft.Lines.Should().HaveCount(2);
|
|
response.Data.UpdateJournalEntryDraft.Lines![0].DebitAmount.Should().Be(1000m);
|
|
response.Data.UpdateJournalEntryDraft.Lines[1].CreditAmount.Should().Be(1000m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_UpdateJournalEntryDraft_FailsForNonExistentDraft()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Non-existent Draft Company");
|
|
var nonExistentId = $"journalentrydraft-{Guid.NewGuid():D}";
|
|
|
|
// Act
|
|
var response = await graphqlClient.MutateAsync<UpdateDraftResponse>("""
|
|
mutation UpdateDraft($input: UpdateJournalEntryDraftInput!) {
|
|
updateJournalEntryDraft(input: $input) {
|
|
id
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
id = nonExistentId,
|
|
name = "Test",
|
|
lines = Array.Empty<object>()
|
|
}
|
|
});
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorInfo = string.Join(" ", response.Errors!.Select(e =>
|
|
e.Message + " " + (e.Extensions?.GetValueOrDefault("details")?.ToString() ?? "")));
|
|
errorInfo.Should().Contain("not found");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Query Tests
|
|
|
|
[Fact]
|
|
public async Task Query_JournalEntryDrafts_ReturnsActiveDrafts()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Query Drafts Test Company");
|
|
|
|
// Create multiple drafts
|
|
await CreateDraftAsync(graphqlClient, companyId, "Draft 1");
|
|
await CreateDraftAsync(graphqlClient, companyId, "Draft 2");
|
|
await CreateDraftAsync(graphqlClient, companyId, "Draft 3");
|
|
|
|
// Wait for eventual consistency
|
|
var drafts = await Eventually.GetListAsync(async () =>
|
|
{
|
|
var repo = GetService<IJournalEntryDraftRepository>();
|
|
return await repo.GetActiveByCompanyIdAsync(companyId);
|
|
}, 3, timeout: TimeSpan.FromSeconds(10));
|
|
|
|
// Act
|
|
var response = await graphqlClient.QueryAsync<DraftsResponse>("""
|
|
query Drafts($companyId: ID!) {
|
|
journalEntryDrafts(companyId: $companyId) {
|
|
id
|
|
name
|
|
status
|
|
}
|
|
}
|
|
""",
|
|
new { companyId });
|
|
|
|
// Assert
|
|
response.EnsureNoErrors();
|
|
response.Data!.JournalEntryDrafts.Should().HaveCount(3);
|
|
response.Data.JournalEntryDrafts.Should().AllSatisfy(d => d.Status.Should().Be("active"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Query_JournalEntryDraft_ReturnsSingleDraft()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Single Draft Query Company");
|
|
var draftId = await CreateDraftAsync(graphqlClient, companyId, "Query Test Draft");
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IJournalEntryDraftRepository>();
|
|
return await repo.GetByIdAsync(draftId);
|
|
});
|
|
|
|
// Act
|
|
var response = await graphqlClient.QueryAsync<SingleDraftResponse>("""
|
|
query Draft($id: ID!) {
|
|
journalEntryDraft(id: $id) {
|
|
id
|
|
name
|
|
companyId
|
|
status
|
|
lines {
|
|
lineNumber
|
|
}
|
|
}
|
|
}
|
|
""",
|
|
new { id = draftId });
|
|
|
|
// Assert
|
|
response.EnsureNoErrors();
|
|
response.Data!.JournalEntryDraft.Should().NotBeNull();
|
|
response.Data.JournalEntryDraft!.Name.Should().Be("Query Test Draft");
|
|
response.Data.JournalEntryDraft.CompanyId.Should().Be(companyId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Query_JournalEntryDrafts_FailsWithoutCompanyHeader()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
// Not setting company header
|
|
|
|
// Act
|
|
var response = await graphqlClient.QueryAsync<DraftsResponse>("""
|
|
query Drafts($companyId: ID!) {
|
|
journalEntryDrafts(companyId: $companyId) {
|
|
id
|
|
}
|
|
}
|
|
""",
|
|
new { companyId = "some-id" });
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Discard Draft Tests
|
|
|
|
[Fact]
|
|
public async Task Mutation_DiscardJournalEntryDraft_DiscardsSuccessfully()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Discard Draft Company");
|
|
var draftId = await CreateDraftAsync(graphqlClient, companyId, "To Be Discarded");
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IJournalEntryDraftRepository>();
|
|
return await repo.GetByIdAsync(draftId);
|
|
});
|
|
|
|
// Act
|
|
var response = await graphqlClient.MutateAsync<DiscardDraftResponse>("""
|
|
mutation DiscardDraft($id: ID!) {
|
|
discardJournalEntryDraft(id: $id) {
|
|
id
|
|
status
|
|
}
|
|
}
|
|
""",
|
|
new { id = draftId });
|
|
|
|
// Assert
|
|
response.EnsureNoErrors();
|
|
response.Data!.DiscardJournalEntryDraft!.Status.Should().Be("discarded");
|
|
|
|
// Verify it's no longer in active drafts
|
|
var activeDrafts = await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IJournalEntryDraftRepository>();
|
|
var drafts = await repo.GetActiveByCompanyIdAsync(companyId);
|
|
return drafts.Any(d => d.Id == draftId) ? null : drafts;
|
|
});
|
|
|
|
activeDrafts.Should().NotContain(d => d.Id == draftId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_DiscardJournalEntryDraft_FailsForAlreadyDiscarded()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Double Discard Company");
|
|
var draftId = await CreateDraftAsync(graphqlClient, companyId, "Discard Twice");
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IJournalEntryDraftRepository>();
|
|
return await repo.GetByIdAsync(draftId);
|
|
});
|
|
|
|
// Discard once
|
|
await graphqlClient.MutateAsync<DiscardDraftResponse>("""
|
|
mutation DiscardDraft($id: ID!) {
|
|
discardJournalEntryDraft(id: $id) { id status }
|
|
}
|
|
""",
|
|
new { id = draftId });
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IJournalEntryDraftRepository>();
|
|
var draft = await repo.GetByIdAsync(draftId);
|
|
return draft?.Status == "discarded" ? draft : null;
|
|
});
|
|
|
|
// Act - Try to discard again
|
|
var response = await graphqlClient.MutateAsync<DiscardDraftResponse>("""
|
|
mutation DiscardDraft($id: ID!) {
|
|
discardJournalEntryDraft(id: $id) { id status }
|
|
}
|
|
""",
|
|
new { id = draftId });
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorInfo = string.Join(" ", response.Errors!.Select(e =>
|
|
e.Message + " " + (e.Extensions?.GetValueOrDefault("details")?.ToString() ?? "")));
|
|
errorInfo.Should().Contain("has been discarded");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Post Draft Tests
|
|
|
|
[Fact]
|
|
public async Task Mutation_PostJournalEntryDraft_FailsWithUnbalancedLines()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Unbalanced Post Company");
|
|
var draftId = await CreateDraftAsync(graphqlClient, companyId, "Unbalanced Draft");
|
|
|
|
// Wait for standard accounts
|
|
var accounts = await Eventually.GetListAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
return await repo.GetActiveByCompanyIdAsync(companyId);
|
|
}, 10, timeout: TimeSpan.FromSeconds(30));
|
|
|
|
var accountId1 = accounts.First().Id;
|
|
var accountId2 = accounts.Skip(1).First().Id;
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IJournalEntryDraftRepository>();
|
|
return await repo.GetByIdAsync(draftId);
|
|
});
|
|
|
|
// Create fiscal year for posting
|
|
var fiscalYearId = await CreateFiscalYearAsync(graphqlClient, companyId);
|
|
|
|
// Update with unbalanced lines
|
|
await graphqlClient.MutateAsync<UpdateDraftResponse>("""
|
|
mutation UpdateDraft($input: UpdateJournalEntryDraftInput!) {
|
|
updateJournalEntryDraft(input: $input) { id }
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
id = draftId,
|
|
fiscalYearId,
|
|
lines = new[]
|
|
{
|
|
new { lineNumber = 1, accountId = accountId1, debitAmount = 1000m, creditAmount = 0m },
|
|
new { lineNumber = 2, accountId = accountId2, debitAmount = 0m, creditAmount = 500m } // Unbalanced!
|
|
}
|
|
}
|
|
});
|
|
|
|
await Task.Delay(500); // Wait for update
|
|
|
|
// Act - Try to post unbalanced draft
|
|
var response = await graphqlClient.MutateAsync<PostDraftResponse>("""
|
|
mutation PostDraft($id: ID!) {
|
|
postJournalEntryDraft(id: $id) {
|
|
id
|
|
status
|
|
transactionId
|
|
}
|
|
}
|
|
""",
|
|
new { id = draftId });
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorInfo = string.Join(" ", response.Errors!.Select(e =>
|
|
e.Message + " " + (e.Extensions?.GetValueOrDefault("details")?.ToString() ?? "")));
|
|
errorInfo.Should().Contain("must equal credits");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_PostJournalEntryDraft_FailsWithoutFiscalYear()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "No Fiscal Year Company");
|
|
var draftId = await CreateDraftAsync(graphqlClient, companyId, "No Fiscal Year Draft");
|
|
|
|
// Wait for standard accounts
|
|
var accounts = await Eventually.GetListAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
return await repo.GetActiveByCompanyIdAsync(companyId);
|
|
}, 10, timeout: TimeSpan.FromSeconds(30));
|
|
|
|
var accountId1 = accounts.First().Id;
|
|
var accountId2 = accounts.Skip(1).First().Id;
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IJournalEntryDraftRepository>();
|
|
return await repo.GetByIdAsync(draftId);
|
|
});
|
|
|
|
// Update with balanced lines but NO fiscal year
|
|
await graphqlClient.MutateAsync<UpdateDraftResponse>("""
|
|
mutation UpdateDraft($input: UpdateJournalEntryDraftInput!) {
|
|
updateJournalEntryDraft(input: $input) { id }
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
id = draftId,
|
|
// No fiscalYearId!
|
|
lines = new[]
|
|
{
|
|
new { lineNumber = 1, accountId = accountId1, debitAmount = 1000m, creditAmount = 0m },
|
|
new { lineNumber = 2, accountId = accountId2, debitAmount = 0m, creditAmount = 1000m }
|
|
}
|
|
}
|
|
});
|
|
|
|
await Task.Delay(500);
|
|
|
|
// Act
|
|
var response = await graphqlClient.MutateAsync<PostDraftResponse>("""
|
|
mutation PostDraft($id: ID!) {
|
|
postJournalEntryDraft(id: $id) {
|
|
id
|
|
status
|
|
}
|
|
}
|
|
""",
|
|
new { id = draftId });
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorInfo = string.Join(" ", response.Errors!.Select(e =>
|
|
e.Message + " " + (e.Extensions?.GetValueOrDefault("details")?.ToString() ?? "")));
|
|
errorInfo.Should().Contain("Fiscal year is required");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_PostJournalEntryDraft_FailsWithMissingAccounts()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Missing Account Company");
|
|
var draftId = await CreateDraftAsync(graphqlClient, companyId, "Missing Account Draft");
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IJournalEntryDraftRepository>();
|
|
return await repo.GetByIdAsync(draftId);
|
|
});
|
|
|
|
var fiscalYearId = await CreateFiscalYearAsync(graphqlClient, companyId);
|
|
|
|
// Update with balanced lines but missing account IDs
|
|
await graphqlClient.MutateAsync<UpdateDraftResponse>("""
|
|
mutation UpdateDraft($input: UpdateJournalEntryDraftInput!) {
|
|
updateJournalEntryDraft(input: $input) { id }
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
id = draftId,
|
|
fiscalYearId,
|
|
lines = new[]
|
|
{
|
|
new { lineNumber = 1, accountId = (string?)null, debitAmount = 1000m, creditAmount = 0m },
|
|
new { lineNumber = 2, accountId = (string?)null, debitAmount = 0m, creditAmount = 1000m }
|
|
}
|
|
}
|
|
});
|
|
|
|
await Task.Delay(500);
|
|
|
|
// Act
|
|
var response = await graphqlClient.MutateAsync<PostDraftResponse>("""
|
|
mutation PostDraft($id: ID!) {
|
|
postJournalEntryDraft(id: $id) {
|
|
id
|
|
}
|
|
}
|
|
""",
|
|
new { id = draftId });
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorInfo = string.Join(" ", response.Errors!.Select(e =>
|
|
e.Message + " " + (e.Extensions?.GetValueOrDefault("details")?.ToString() ?? "")));
|
|
errorInfo.Should().Contain("must have an account");
|
|
}
|
|
|
|
[Fact(Skip = "Requires Ledger period setup which is complex; domain logic is tested in JournalEntryDraftAggregateTests")]
|
|
public async Task Mutation_UpdateJournalEntryDraft_FailsForPostedDraft()
|
|
{
|
|
// Arrange - Create and post a draft first
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Posted Update Company");
|
|
var draftId = await CreateDraftAsync(graphqlClient, companyId, "To Be Posted Then Updated");
|
|
|
|
// Wait for accounts
|
|
var accounts = await Eventually.GetListAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
return await repo.GetActiveByCompanyIdAsync(companyId);
|
|
}, 10, timeout: TimeSpan.FromSeconds(30));
|
|
|
|
var accountId1 = accounts.First().Id;
|
|
var accountId2 = accounts.Skip(1).First().Id;
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IJournalEntryDraftRepository>();
|
|
return await repo.GetByIdAsync(draftId);
|
|
});
|
|
|
|
var fiscalYearId = await CreateFiscalYearAsync(graphqlClient, companyId);
|
|
|
|
// Update draft with valid data for posting
|
|
await graphqlClient.MutateAsync<UpdateDraftResponse>("""
|
|
mutation UpdateDraft($input: UpdateJournalEntryDraftInput!) {
|
|
updateJournalEntryDraft(input: $input) { id }
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
id = draftId,
|
|
fiscalYearId,
|
|
lines = new[]
|
|
{
|
|
new { lineNumber = 1, accountId = accountId1, debitAmount = 1000m, creditAmount = 0m },
|
|
new { lineNumber = 2, accountId = accountId2, debitAmount = 0m, creditAmount = 1000m }
|
|
}
|
|
}
|
|
});
|
|
|
|
await Task.Delay(500);
|
|
|
|
// Post the draft
|
|
var postResponse = await graphqlClient.MutateAsync<PostDraftResponse>("""
|
|
mutation PostDraft($id: ID!) {
|
|
postJournalEntryDraft(id: $id) { id status }
|
|
}
|
|
""",
|
|
new { id = draftId });
|
|
|
|
postResponse.EnsureNoErrors();
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IJournalEntryDraftRepository>();
|
|
var draft = await repo.GetByIdAsync(draftId);
|
|
return draft?.Status == "posted" ? draft : null;
|
|
});
|
|
|
|
// Act - Try to update the posted draft
|
|
var response = await graphqlClient.MutateAsync<UpdateDraftResponse>("""
|
|
mutation UpdateDraft($input: UpdateJournalEntryDraftInput!) {
|
|
updateJournalEntryDraft(input: $input) { id }
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
id = draftId,
|
|
name = "Should Fail",
|
|
lines = Array.Empty<object>()
|
|
}
|
|
});
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorInfo = string.Join(" ", response.Errors!.Select(e =>
|
|
e.Message + " " + (e.Extensions?.GetValueOrDefault("details")?.ToString() ?? "")));
|
|
errorInfo.Should().Contain("already been posted");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private async Task<string> CreateCompanyAsync(GraphQLTestClient client, string name)
|
|
{
|
|
var response = await client.MutateAsync<CreateCompanyResponse>("""
|
|
mutation CreateCompany($input: CreateCompanyInput!) {
|
|
createCompany(input: $input) { id }
|
|
}
|
|
""",
|
|
new { input = new { name } });
|
|
|
|
response.EnsureNoErrors();
|
|
var companyId = response.Data!.CreateCompany!.Id;
|
|
client.SetCompanyId(companyId);
|
|
return companyId;
|
|
}
|
|
|
|
private async Task<string> CreateDraftAsync(GraphQLTestClient client, string companyId, string name)
|
|
{
|
|
var response = await client.MutateAsync<CreateDraftResponse>("""
|
|
mutation CreateDraft($input: CreateJournalEntryDraftInput!) {
|
|
createJournalEntryDraft(input: $input) { id }
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
name
|
|
}
|
|
});
|
|
|
|
response.EnsureNoErrors();
|
|
return response.Data!.CreateJournalEntryDraft!.Id;
|
|
}
|
|
|
|
private async Task<string> CreateFiscalYearAsync(GraphQLTestClient client, string companyId)
|
|
{
|
|
var response = await client.MutateAsync<CreateFiscalYearResponse>("""
|
|
mutation CreateFiscalYear($input: CreateFiscalYearInput!) {
|
|
createFiscalYear(input: $input) { id }
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
name = "2025",
|
|
startDate = "2025-01-01T00:00:00",
|
|
endDate = "2025-12-31T00:00:00",
|
|
isFirstFiscalYear = true
|
|
}
|
|
});
|
|
|
|
response.EnsureNoErrors();
|
|
var fiscalYearId = response.Data!.CreateFiscalYear!.Id;
|
|
|
|
// Wait for fiscal year to be created
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IFiscalYearRepository>();
|
|
return await repo.GetByIdAsync(fiscalYearId);
|
|
});
|
|
|
|
return fiscalYearId;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Response DTOs
|
|
|
|
private class CreateDraftResponse { public DraftDto? CreateJournalEntryDraft { get; set; } }
|
|
private class UpdateDraftResponse { public DraftDto? UpdateJournalEntryDraft { get; set; } }
|
|
private class PostDraftResponse { public DraftDto? PostJournalEntryDraft { get; set; } }
|
|
private class DiscardDraftResponse { public DraftDto? DiscardJournalEntryDraft { get; set; } }
|
|
private class DraftsResponse { public List<DraftDto> JournalEntryDrafts { get; set; } = []; }
|
|
private class SingleDraftResponse { public DraftDto? JournalEntryDraft { get; set; } }
|
|
private class CreateCompanyResponse { public CompanyDto? CreateCompany { get; set; } }
|
|
private class CreateFiscalYearResponse { public FiscalYearDto? CreateFiscalYear { get; set; } }
|
|
|
|
private class DraftDto
|
|
{
|
|
public string Id { get; set; } = string.Empty;
|
|
public string CompanyId { get; set; } = string.Empty;
|
|
public string Name { get; set; } = string.Empty;
|
|
public string? DocumentDate { get; set; }
|
|
public string? Description { get; set; }
|
|
public string? FiscalYearId { get; set; }
|
|
public string Status { get; set; } = string.Empty;
|
|
public string? TransactionId { get; set; }
|
|
public string CreatedBy { get; set; } = string.Empty;
|
|
public List<DraftLineDto>? Lines { get; set; }
|
|
}
|
|
|
|
private class DraftLineDto
|
|
{
|
|
public int LineNumber { get; set; }
|
|
public string? AccountId { get; set; }
|
|
public decimal DebitAmount { get; set; }
|
|
public decimal CreditAmount { get; set; }
|
|
public string? Description { get; set; }
|
|
}
|
|
|
|
private class CompanyDto { public string Id { get; set; } = string.Empty; }
|
|
private class FiscalYearDto { public string Id { get; set; } = string.Empty; }
|
|
|
|
#endregion
|
|
}
|