Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions src/ArlaNatureConnect.Core/Abstract/ICreateNatureCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using ArlaNatureConnect.Domain.Entities;

namespace ArlaNatureConnect.Core.Abstract;

public interface ICreateNatureCheck
{
Task<List<Farm>> GetFarmsAsync();
Task<List<Person>> GetPersonsAsync();
Task<List<NatureCheckCase>> GetNatureChecksAsync();
Task<Guid> CreateNatureCheckAsync(CreateNatureCheck request, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using ArlaNatureConnect.Domain.Entities;

namespace ArlaNatureConnect.Core.Abstract;

public interface ICreateNatureCheckRepository
{
Task<Guid> CreateNatureCheckAsync(CreateNatureCheck request, CancellationToken cancellationToken = default);

Task<CreateNatureCheck?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
}
14 changes: 14 additions & 0 deletions src/ArlaNatureConnect.Core/Abstract/IEmailService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace ArlaNatureConnect.Core.Abstract;

public interface IEmailService
{
Task SendNatureCheckCreatedEmailAsync(
string toEmail,
string consultantName,
string farmName,
string farmAddress,
string farmCvr,
DateTime dateTime,
Guid natureCheckId,
CancellationToken cancellationToken = default);
}
87 changes: 87 additions & 0 deletions src/ArlaNatureConnect.Core/Services/CreateNatureCheckService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using ArlaNatureConnect.Core.Abstract;
using ArlaNatureConnect.Domain.Entities;

namespace ArlaNatureConnect.Core.Services;

public class CreateNatureCheckService : ICreateNatureCheck
{
private readonly IFarmRepository _farmRepository;
private readonly IPersonRepository _personRepository;
private readonly INatureCheckCaseRepository _natureCheckCaseRepository;
private readonly ICreateNatureCheckRepository _createNatureCheckRepository;
private readonly IEmailService _emailService;

public CreateNatureCheckService(
IFarmRepository farmRepository,
IPersonRepository personRepository,
INatureCheckCaseRepository natureCheckCaseRepository,
ICreateNatureCheckRepository createNatureCheckRepository,
IEmailService emailService)
{
_farmRepository = farmRepository ?? throw new ArgumentNullException(nameof(farmRepository));
_personRepository = personRepository ?? throw new ArgumentNullException(nameof(personRepository));
_natureCheckCaseRepository = natureCheckCaseRepository ?? throw new ArgumentNullException(nameof(natureCheckCaseRepository));
_createNatureCheckRepository = createNatureCheckRepository ?? throw new ArgumentNullException(nameof(createNatureCheckRepository));
_emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
}

public async Task<List<Farm>> GetFarmsAsync()
{
var farms = await _farmRepository.GetAllAsync();
return farms.OrderBy(f => f.Name).ToList();
}

public async Task<List<Person>> GetPersonsAsync()
{
var persons = await _personRepository.GetAllAsync();
return persons.OrderBy(p => p.LastName).ToList();
}

public async Task<List<NatureCheckCase>> GetNatureChecksAsync()
{
var checks = await _natureCheckCaseRepository.GetAllAsync();
return checks.OrderByDescending(n => n.CreatedAt).ToList();
}

public async Task<Guid> CreateNatureCheckAsync(
CreateNatureCheck request,
CancellationToken cancellationToken = default)
{
if (request is null)
throw new ArgumentNullException(nameof(request));

// 1) Save to DB (via stored procedure)
var id = await _createNatureCheckRepository.CreateNatureCheckAsync(request, cancellationToken);

// 2) Load extra data for email (consultant email + farm info)
var consultant = await _personRepository.GetByIdAsync(request.PersonId);
var farm = await _farmRepository.GetByIdAsync(request.FarmId);

var toEmail = consultant?.Email;
var consultantName = consultant is null
? $"{request.ConsultantFirstName} {request.ConsultantLastName}"
: $"{consultant.FirstName} {consultant.LastName}";

var farmName = farm?.Name ?? request.FarmName;
var farmAddress = request.FarmAddress;
var farmCvr = (farm?.CVR ?? request.FarmCVR.ToString());

// 3) Send email (do NOT fail the operation if email fails)
try
{
await _emailService.SendNatureCheckCreatedEmailAsync(
toEmail: toEmail ?? string.Empty,
consultantName: consultantName,
farmName: farmName,
farmAddress: farmAddress,
farmCvr: farmCvr,
dateTime: request.DateTime,
natureCheckId: id,
cancellationToken: cancellationToken);
}
catch
{
}
return id;
}
}
15 changes: 15 additions & 0 deletions src/ArlaNatureConnect.Domain/Entities/CreateNatureCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace ArlaNatureConnect.Domain.Entities;

public class CreateNatureCheck
{
public Guid NatureCheckId { get; set; }
public Guid FarmId { get; set; }
public Guid PersonId { get; set; }
public string FarmName { get; set; }
public int FarmCVR { get; set; }
public string FarmAddress { get; set; }
public string ConsultantFirstName { get; set; }
public string ConsultantLastName { get; set; }
public DateTime DateTime { get; set; }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Net;
using System.Net.Mail;

using ArlaNatureConnect.Core.Abstract;

namespace ArlaNatureConnect.Infrastructure.ExternalServices;

public class SmtpEmailService : IEmailService
{
private readonly SmtpSettings _settings;

public SmtpEmailService(SmtpSettings settings)
{
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
}

public async Task SendNatureCheckCreatedEmailAsync(
string toEmail,
string consultantName,
string farmName,
string farmAddress,
string farmCvr,
DateTime dateTime,
Guid natureCheckId,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(toEmail))
return;

var subject = "Nyt naturtjek oprettet";
var body = $@"
Hej {consultantName},

Der er lige blevet oprettet et nyt naturtjek.

Detaljer:
- Naturtjek ID: {natureCheckId}
- Gård: {farmName}
- CVR: {farmCvr}
- Adresse: {farmAddress}
- Dato/Tid: {dateTime:dd-MM-yyyy HH:mm}

Venlig hilsen
Arla NatureConnect
";

using var message = new MailMessage
{
From = new MailAddress(_settings.FromEmail, _settings.FromName),
Subject = subject,
Body = body,
IsBodyHtml = false
};

message.To.Add(toEmail);

using var client = new SmtpClient(_settings.Host, _settings.Port)
{
EnableSsl = _settings.EnableSsl,
Credentials = new NetworkCredential(_settings.Username, _settings.Password)
};


await client.SendMailAsync(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace ArlaNatureConnect.Infrastructure.ExternalServices;

public class SmtpSettings
{
public string Host { get; init; } = default!;
public int Port { get; init; }
public bool EnableSsl { get; init; }
public string Username { get; init; } = default!;
public string Password { get; init; } = default!;
public string FromEmail { get; init; } = default!;
public string FromName { get; init; } = "Arla NatureConnect";
}
53 changes: 46 additions & 7 deletions src/ArlaNatureConnect.Infrastructure/Persistence/AppDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,56 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
// 1. Farms may have Guid.Empty for PersonId/AddressId (temporary state during creation)
// 2. AutoInclude would fail when trying to load non-existent entities
// Use explicit .Include() in queries when needed
// Note continued:
// Should be handled anotherway when Guid.Empty as it cannot be applied in FK relationship
// by using a placeholder before saving and get outputed Guid from DB.

// Important:
// Please keep navigation auto-includes here in sync with Repository.cs to ensure
// consistent behavior across the application.
modelBuilder.Entity<Farm>().Navigation(e => e.Owner).AutoInclude();
modelBuilder.Entity<Farm>().Navigation(e => e.Address).AutoInclude();

modelBuilder.Entity<NatureArea>().Navigation(e => e.Coordinates).AutoInclude();
modelBuilder.Entity<NatureArea>().Navigation(e => e.Images).AutoInclude();

// Ensure EF includes navigation properties for NatureCheckCase so UI bindings to Farm/Consultant work
modelBuilder.Entity<NatureCheckCase>().Navigation(n => n.Farm).AutoInclude();
modelBuilder.Entity<NatureCheckCase>().Navigation(n => n.Consultant).AutoInclude();
// NOTE: AssignedByPerson navigation is NOT auto-included because legacy dbo.NatureCheck does not have AssignedByPersonId

// Configure NatureCheckCase mapping to match the database table name used in the user's DB
modelBuilder.Entity<NatureCheckCase>(entity =>
{
// The user's database table is named 'NatureCheck'
entity.ToTable("NatureCheck");

entity.HasKey(n => n.Id);

// Map CLR property CreatedAt to SQL column 'Date' if present
entity.Property(n => n.CreatedAt).HasColumnName("Date");

// Map ConsultantId to the PersonId column used in the legacy table
entity.Property(n => n.ConsultantId).HasColumnName("PersonId");

// Some older DB schemas do not contain these columns; ignore them to avoid SQL errors
entity.Ignore(n => n.AssignedAt);
entity.Ignore(n => n.AssignedByPersonId);
entity.Ignore(n => n.Notes);
entity.Ignore(n => n.Priority);
entity.Ignore(n => n.Status);

// Also ignore the navigation for AssignedByPerson so EF will not create a shadow FK
entity.Ignore(n => n.AssignedByPerson);

// Ensure foreign keys are configured where possible
entity.HasOne(n => n.Farm)
.WithMany()
.HasForeignKey(n => n.FarmId)
.OnDelete(DeleteBehavior.Cascade);

entity.HasOne(n => n.Consultant)
.WithMany()
.HasForeignKey(n => n.ConsultantId)
.OnDelete(DeleteBehavior.NoAction);

// Do not map AssignedByPerson foreign-key because the legacy table does not contain that column.
// If the column is added to the DB in future, remove the Ignore above and reintroduce the FK mapping.

// Do not enforce column mappings for other optional properties here; rely on convention or add mappings later if needed.
});
}
}
Loading