Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

πŸ–‹οΈ feat: Transform signature to value object #162

Merged
merged 27 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
29e01b6
:memo: docs: clarify aggregate root description
kamilbaczek Aug 31, 2024
6f7f957
feat: fix all integrations test to support signature
kamilbaczek Oct 15, 2024
0f64d27
Merge branch 'main' into feature/aggregate_root_description_clarifying
kamilbaczek Oct 15, 2024
294a034
Update action.yml
kamilbaczek Oct 16, 2024
373fdaf
Update action.yml
kamilbaczek Oct 16, 2024
d391edd
Update action.yml
kamilbaczek Oct 16, 2024
89acb55
Update action.yml
kamilbaczek Oct 16, 2024
945e712
Update action.yml
kamilbaczek Oct 16, 2024
9c757d2
Update action.yml
kamilbaczek Oct 17, 2024
c4e6140
Update chapter-4-contracts-workflow.yml
kamilbaczek Oct 17, 2024
46aa898
Update action.yml
kamilbaczek Oct 17, 2024
c489294
Update action.yml
kamilbaczek Oct 17, 2024
48f05fe
Update chapter-4-contracts-workflow.yml
kamilbaczek Oct 17, 2024
a8b4fac
Update appsettings.json
kamilbaczek Oct 17, 2024
f3034ac
fix: restore unessery changes
kamilbaczek Oct 17, 2024
fce8587
feat: add unit test that covers to long text
kamilbaczek Oct 17, 2024
108b2e6
feat: add tests for signature
kamilbaczek Oct 17, 2024
b63b4c3
Merge branch 'feature/add_signature_value_object' of https://github.c…
kamilbaczek Oct 17, 2024
577b611
feat: add unit tests
kamilbaczek Oct 18, 2024
b29e8cb
fix: remove part of using
kamilbaczek Oct 19, 2024
08934db
feat: apply fixes after review
kamilbaczek Oct 26, 2024
59cf8e0
refactor: using Microsoft.AspNetCore.Mvc to global
kamilbaczek Oct 26, 2024
bacdb00
feat: add contracts
kamilbaczek Oct 26, 2024
907d133
feat: fix migration
kamilbaczek Oct 26, 2024
8c75e5a
refactor: remove unessery usings
kamilbaczek Oct 26, 2024
a2b3f76
Update ContractEntityConfiguration.cs
kamilbaczek Oct 26, 2024
47005f3
Update Fitnet.Contracts.Infrastructure.csproj
kamilbaczek Oct 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ runs:
run: |
cd ${{ github.workspace }}/${{ inputs.path }}

if dotnet nuget list source | grep -q $NugetSourceName; then
echo "Removing existing nuget source: $NugetSourceName"
dotnet nuget remove source $NugetSourceName
if dotnet nuget list source | grep -q ${{ inputs.nuget-source-name }}; then
echo "Removing existing nuget source: '${{ inputs.nuget-source-name }}'"
dotnet nuget remove source '${{ inputs.nuget-source-name }}'
else
echo "Nuget source $NugetSourceName does not exist. Skipping removal."
fi

dotnet nuget add source --username ${{ inputs.owner }} --password ${{ inputs.github-token }} --store-password-in-clear-text --name ${{ inputs.nuget-source-name }} "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json"
dotnet nuget list source
shell: bash
shell: bash
4 changes: 2 additions & 2 deletions .github/workflows/chapter-4-contracts-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
with:
dotnet-version: 8.0.x
- name: Add Evolutionary Architecture Nuget Source
uses: evolutionary-architecture/evolutionary-architecture-by-example/.github@main
uses: evolutionary-architecture/evolutionary-architecture-by-example/.github@feature/add_signature_value_object
kamilbaczek marked this conversation as resolved.
Show resolved Hide resolved
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
owner: ${{ github.repository_owner }}
Expand All @@ -54,7 +54,7 @@ jobs:
with:
dotnet-version: 8.0.x
- name: Add Evolutionary Architecture Nuget Source
uses: evolutionary-architecture/evolutionary-architecture-by-example/.github@main
uses: evolutionary-architecture/evolutionary-architecture-by-example/.github@feature/add_signature_value_object
kamilbaczek marked this conversation as resolved.
Show resolved Hide resolved
kamilbaczek marked this conversation as resolved.
Show resolved Hide resolved
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
owner: ${{ github.repository_owner }}
Expand Down
20 changes: 4 additions & 16 deletions Chapter-2-modules-separation/Src/Fitnet/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,16 @@
"AllowedHosts": "*",
"Modules": {
"Passes": {
"Enabled": true,
"ConnectionStrings": {
"Primary": ""
}
"Enabled": false
},
"Contracts": {
"Enabled": true,
"ConnectionStrings": {
"Primary": ""
}
"Enabled": true
},
"Reports": {
"Enabled": true,
"ConnectionStrings": {
"Primary": ""
}
"Enabled": false
},
"Offers": {
"Enabled": true,
"ConnectionStrings": {
"Primary": ""
}
"Enabled": false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ namespace EvolutionaryArchitecture.Fitnet.Contracts.Api.UnitTests.SignContract;

public sealed class SignContractRequestValidatorTests
{
private const string ValidSignatureText = "John Doe";
private readonly SignContractRequestValidator _validator = new();
private readonly DateTimeOffset _fakeNow = new Faker().Date.RecentOffset();

[Fact]
internal void Given_sign_contract_request_validation_When_request_is_valid_Then_result_should_have_no_errors()
{
// Arrange
var request = new SignContractRequest(_fakeNow);
var request = new SignContractRequest(_fakeNow, ValidSignatureText);

// Act
var result = _validator.TestValidate(request);
Expand All @@ -25,7 +26,7 @@ internal void Given_sign_contract_request_validation_When_request_is_valid_Then_
internal void Given_sign_contract_request_validation_When_signed_at_not_provided_Then_result_should_have_error()
{
// Arrange
var request = new SignContractRequest(default);
var request = new SignContractRequest(default, ValidSignatureText);

// Act
var result = _validator.TestValidate(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ internal static class PrepareContractEndpoint
{
internal static void MapPrepareContract(this IEndpointRouteBuilder app) =>
app.MapPost(ContractsApiPaths.Prepare,
async Task (PrepareContractRequest request, IContractsModule contractsModule, CancellationToken cancellationToken) =>
await contractsModule.ExecuteCommandAsync(request.ToCommand(), cancellationToken).Match(
contractId => Results.Created(ContractsApiPaths.GetPreparedContractPath(contractId), (object?)contractId),
async Task<IResult> (PrepareContractRequest request, IContractsModule contractsModule, CancellationToken cancellationToken) =>
await contractsModule.ExecuteCommandAsync(request.ToCommand(), cancellationToken)
.Match(
contractId =>
{
var uri = ContractsApiPaths.GetPreparedContractPath(contractId);
var value = (object?)contractId;
var response = Results.Created(uri, value);

return response;
},
errors => errors.ToProblem()))
.ValidateRequest<PrepareContractRequest>()
.WithOpenApi(operation => new(operation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ namespace EvolutionaryArchitecture.Fitnet.Contracts.Api.SignContract;

internal static class SignContractEndpoint
{
internal static void MapSignContract(this IEndpointRouteBuilder app) => app.MapPatch(ContractsApiPaths.Sign, async (
internal static void MapSignContract(this IEndpointRouteBuilder app) => app.MapPatch(ContractsApiPaths.Sign,
async Task<IResult> (
Guid id,
SignContractRequest request,
IContractsModule contractsModule, CancellationToken cancellationToken) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ namespace EvolutionaryArchitecture.Fitnet.Contracts.Api.SignContract;

using EvolutionaryArchitecture.Fitnet.Contracts.Application.SignContract;

internal sealed record SignContractRequest(DateTimeOffset SignedAt)
internal sealed record SignContractRequest(DateTimeOffset SignedAt, string SignatureText)
{
internal SignContractCommand ToCommand(Guid id) =>
new(id, SignedAt);
new(id, SignatureText, SignedAt);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ namespace EvolutionaryArchitecture.Fitnet.Contracts.Api.SignContract;

internal sealed class SignContractRequestValidator : AbstractValidator<SignContractRequest>
{
public SignContractRequestValidator() => RuleFor(signContractRequest => signContractRequest.SignedAt)
.NotEmpty();
public SignContractRequestValidator()
{
RuleFor(signContractRequest => signContractRequest.SignatureText)
.NotEmpty()
.MaximumLength(100);

RuleFor(signContractRequest => signContractRequest.SignedAt)
.NotEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
namespace EvolutionaryArchitecture.Fitnet.Contracts.Application.SignContract;

public sealed record SignContractCommand(Guid Id, DateTimeOffset SignedAt) : ICommand<ErrorOr<Guid>>;
public sealed record SignContractCommand(Guid Id, string SignatureText, DateTimeOffset SignedAt) : ICommand<ErrorOr<Guid>>;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace EvolutionaryArchitecture.Fitnet.Contracts.Application.SignContract;

using Core;
using IntegrationEvents;
using MassTransit;

Expand All @@ -12,14 +13,14 @@ internal sealed class SignContractCommandHandler(
{
public async Task<ErrorOr<Guid>> Handle(SignContractCommand command, CancellationToken cancellationToken) =>
await contractsRepository.GetByIdAsync(command.Id, cancellationToken)
.ThenAsync(async contract => await contract.Sign(command.SignedAt, timeProvider.GetUtcNow())
.ThenAsync(async contract => await contract.Sign(Signature.From(command.SignedAt, command.SignatureText), timeProvider.GetUtcNow())
.ThenAsync(async bindingContract =>
{
await bindingContractsRepository.AddAsync(bindingContract, cancellationToken);
await contractsRepository.CommitAsync(cancellationToken);
var @event = ContractSignedEvent.Create(contract.Id.Value,
contract.CustomerId,
contract.SignedAt!.Value,
contract.Signature!.Date,
contract.ExpiringAt!.Value);
await publishEndpoint.Publish(@event, cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public SignContractBuilder SignedOn(DateTimeOffset signDay, DateTimeOffset fakeT

private BindingContract Build()
{
var bindingContract = parentBuilder.Sign(_signDay, _fakeToday);
var signature = Signature.From(_signDay, "John Doe");
var bindingContract = parentBuilder.Sign(signature, _fakeToday);

return bindingContract.Value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ namespace EvolutionaryArchitecture.Fitnet.Contracts.Core.UnitTests.SignContract;

public sealed class SignContractTests
{
private const string SignatureText = "John Doe";

[Theory]
[ClassData(typeof(SignContractTestData))]
internal void Given_sign_contract_Then_expiration_date_is_set_to_contract_duration_from_now(
Expand All @@ -18,9 +20,10 @@ internal void Given_sign_contract_Then_expiration_date_is_set_to_contract_durati
Contract contract = ContractBuilder
.Prepared()
.PreparedAt(preparedAt);
var signature = Signature.From(signedAt, SignatureText);

// Act
var signResult = contract.Sign(signedAt, fakeNow);
var signResult = contract.Sign(signature, fakeNow);

// Assert
var @event = signResult.Value.GetPublishedEvent<BindingContractStartedEvent>();
Expand All @@ -36,9 +39,10 @@ internal void Given_sign_contract_Then_contracts_becomes_binding_contract()
// Arrange
Contract contract = ContractBuilder
.Prepared();
var signature = Signature.From(SignedAt, SignatureText);

// Act
var signResult = contract.Sign(SignedAt, FakeNow);
var signResult = contract.Sign(signature, FakeNow);

// Assert
var @event = signResult.Value.GetPublishedEvent<BindingContractStartedEvent>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

internal sealed class SignedContractBuilder(Contract parentBuilder)
{
private const string SignatureText = "John Doe";
private DateTimeOffset? _signDay;
private DateTimeOffset? _fakeToday;

Expand All @@ -19,7 +20,8 @@ private BindingContract Build()
{
var signDay = _signDay ?? FakeContractDates.SignDay;
var fakeToday = _fakeToday ?? FakeContractDates.SignDay;
var bindingContract = parentBuilder.Sign(signDay, fakeToday).Value;
var signature = Signature.From(signDay, SignatureText);
var bindingContract = parentBuilder.Sign(signature, fakeToday).Value;

return bindingContract;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ public sealed class Contract : Entity
public DateTimeOffset PreparedAt { get; init; }
public TimeSpan Duration { get; init; }

public DateTimeOffset? SignedAt { get; private set; }
public Signature? Signature { get; private set; }
public DateTimeOffset? ExpiringAt { get; private set; }

public bool IsSigned => SignedAt.HasValue;
public bool IsSigned => Signature is not null;

// EF needs this constructor to create non-primitive types
private Contract() { }
Expand Down Expand Up @@ -51,13 +51,13 @@ public static ErrorOr<Contract> Prepare(
new PreviousContractHasToBeSignedRule(isPreviousContractSigned))
.Then<Contract>(_ => new Contract(customerId, preparedAt, StandardDuration));

public ErrorOr<BindingContract> Sign(DateTimeOffset signedAt, DateTimeOffset now) =>
public ErrorOr<BindingContract> Sign(Signature signature, DateTimeOffset now) =>
BusinessRuleValidator.Validate(
new ContractMustNotBeAlreadySignedRule(IsSigned),
new ContractCanOnlyBeSignedWithin30DaysFromPreparationRule(PreparedAt, signedAt))
new ContractCanOnlyBeSignedWithin30DaysFromPreparationRule(PreparedAt, signature.Date))
.Then(_ =>
{
SignedAt = signedAt;
Signature = signature;
ExpiringAt = now.Add(Duration);
var bindingContract = BindingContract.Start(Id, CustomerId, Duration, now, ExpiringAt.Value);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
ο»Ώnamespace EvolutionaryArchitecture.Fitnet.Contracts.Core;

using System.Text.RegularExpressions;

public sealed partial class Signature
{
private static readonly Regex SignatureTextPattern = SignatureAllowedCharacters();
public DateTimeOffset Date { get; }
public string Text { get; }

private Signature(DateTimeOffset date, string text)
{
Date = date;
if (!SignatureTextPattern.IsMatch(text))
{
throw new SignatureNotValidException(text);
}

Text = text;
}

public static Signature From(DateTimeOffset signedAt, string signatureText) =>
new(signedAt, signatureText);

[GeneratedRegex(@"^[A-Za-z\s]+$")]
private static partial Regex SignatureAllowedCharacters();
}

public sealed class SignatureNotValidException(string signatureText) : InvalidOperationException($"Signature text: '{signatureText}' contains invalid characters.");
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace EvolutionaryArchitecture.Fitnet.Contracts.Infrastructure.Database.Conf

using Core;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

internal sealed class ContractEntityConfiguration : IEntityTypeConfiguration<Contract>
Expand All @@ -17,6 +18,21 @@ public void Configure(EntityTypeBuilder<Contract> builder)
value => new ContractId(value))
.ValueGeneratedOnAdd();
builder.Property(contract => contract.PreparedAt).IsRequired();
builder.Property(contract => contract.SignedAt).IsRequired(false);
builder.OwnsOne<Signature>("Signature", signatureBuilder =>
{
signatureBuilder.Property(signature => signature.Date).IsRequired();
signatureBuilder.Property(signature => signature.Text).IsRequired().HasMaxLength(100);
});
}
}

public class BloggingContextFactory : IDesignTimeDbContextFactory<ContractsPersistence>
kamilbaczek marked this conversation as resolved.
Show resolved Hide resolved
{
public ContractsPersistence CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<ContractsPersistence>();
optionsBuilder.UseNpgsql("Host=localhost:5432;Database=fitnet;Username=postgres;Password=mysecretpassword");

return new ContractsPersistence(optionsBuilder.Options);
}
}
Loading
Loading