Initial import
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MinAttest.Contracts.Attests;
|
||||
using MinAttest.Application.Abstractions;
|
||||
using MinAttest.Domain.Entities;
|
||||
|
||||
namespace MinAttest.Application.Features.Attests.Commands;
|
||||
|
||||
public record CompleteUploadCommand(Guid UploadId, CompleteUploadRequest Request) : IRequest<Guid?>;
|
||||
|
||||
public class CompleteUploadCommandHandler(IAppDbContext db) : IRequestHandler<CompleteUploadCommand, Guid?>
|
||||
{
|
||||
public async Task<Guid?> Handle(CompleteUploadCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var r = request.Request;
|
||||
var personExists = await db.Persons.AnyAsync(p => p.Id == r.PersonId, cancellationToken);
|
||||
if (!personExists) return null;
|
||||
|
||||
if (r.EmployerId is Guid eid)
|
||||
{
|
||||
var exists = await db.Employers.AnyAsync(e => e.Id == eid, cancellationToken);
|
||||
if (!exists) return null;
|
||||
}
|
||||
|
||||
byte[]? content = null;
|
||||
long? length = null;
|
||||
if (!string.IsNullOrWhiteSpace(r.ContentBase64))
|
||||
{
|
||||
try
|
||||
{
|
||||
content = Convert.FromBase64String(r.ContentBase64);
|
||||
length = content.LongLength;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// invalid base64 -> treat as bad request
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var a = new Attest
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PersonId = r.PersonId,
|
||||
EmployerId = r.EmployerId,
|
||||
Title = r.Title,
|
||||
From = r.From,
|
||||
To = r.To,
|
||||
Summary = r.Summary,
|
||||
BlobPath = r.BlobPath,
|
||||
BlobHash = r.BlobHash,
|
||||
Content = content,
|
||||
ContentType = r.ContentType,
|
||||
ContentLength = length,
|
||||
IssuedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
db.Attests.Add(a);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return a.Id;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace MinAttest.Application.Features.Attests.Commands;
|
||||
|
||||
public class CompleteUploadCommandValidator : AbstractValidator<CompleteUploadCommand>
|
||||
{
|
||||
public CompleteUploadCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Request.PersonId).NotEqual(Guid.Empty);
|
||||
RuleFor(x => x.Request.Title).NotEmpty().MaximumLength(200);
|
||||
RuleFor(x => x.Request.BlobPath).NotEmpty().MaximumLength(500);
|
||||
// Allow either inline content or external reference
|
||||
RuleFor(x => x.Request)
|
||||
.Must(r => !string.IsNullOrWhiteSpace(r.ContentBase64) || !string.IsNullOrWhiteSpace(r.BlobPath))
|
||||
.WithMessage("Either ContentBase64 or BlobPath must be provided");
|
||||
When(x => !string.IsNullOrWhiteSpace(x.Request.ContentBase64), () =>
|
||||
{
|
||||
RuleFor(x => x.Request.ContentType).NotEmpty().MaximumLength(255);
|
||||
});
|
||||
RuleFor(x => x.Request.From).LessThanOrEqualTo(x => x.Request.To);
|
||||
RuleFor(x => x.Request.Summary).MaximumLength(2000).When(x => !string.IsNullOrWhiteSpace(x.Request.Summary));
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MinAttest.Application.Abstractions;
|
||||
using MinAttest.Contracts.Attests;
|
||||
using MinAttest.Domain.Entities;
|
||||
|
||||
namespace MinAttest.Application.Features.Attests.Commands;
|
||||
|
||||
public record EmployerIssueAttestCommand(Guid EmployerId, EmployerAttestUploadRequest Request) : IRequest<Guid?>;
|
||||
|
||||
public class EmployerIssueAttestCommandHandler(IAppDbContext db) : IRequestHandler<EmployerIssueAttestCommand, Guid?>
|
||||
{
|
||||
public async Task<Guid?> Handle(EmployerIssueAttestCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var r = request.Request;
|
||||
var employerExists = await db.Employers.AnyAsync(e => e.Id == request.EmployerId, cancellationToken);
|
||||
if (!employerExists) return null;
|
||||
|
||||
var personExists = await db.Persons.AnyAsync(p => p.Id == r.PersonId, cancellationToken);
|
||||
if (!personExists) return null;
|
||||
|
||||
byte[]? content = null;
|
||||
long? length = null;
|
||||
if (!string.IsNullOrWhiteSpace(r.ContentBase64))
|
||||
{
|
||||
try { content = Convert.FromBase64String(r.ContentBase64); length = content.LongLength; }
|
||||
catch (FormatException) { return null; }
|
||||
}
|
||||
|
||||
var a = new Attest
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PersonId = r.PersonId,
|
||||
EmployerId = request.EmployerId,
|
||||
Title = r.Title,
|
||||
From = r.From,
|
||||
To = r.To,
|
||||
Summary = r.Summary,
|
||||
BlobPath = r.BlobPath,
|
||||
BlobHash = r.BlobHash,
|
||||
Content = content,
|
||||
ContentType = r.ContentType,
|
||||
ContentLength = length,
|
||||
IssuedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
db.Attests.Add(a);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return a.Id;
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace MinAttest.Application.Features.Attests.Commands;
|
||||
|
||||
public class EmployerIssueAttestCommandValidator : AbstractValidator<EmployerIssueAttestCommand>
|
||||
{
|
||||
public EmployerIssueAttestCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.EmployerId).NotEqual(Guid.Empty);
|
||||
RuleFor(x => x.Request.PersonId).NotEqual(Guid.Empty);
|
||||
RuleFor(x => x.Request.Title).NotEmpty().MaximumLength(200);
|
||||
RuleFor(x => x.Request.BlobPath).MaximumLength(500);
|
||||
RuleFor(x => x.Request.From).LessThanOrEqualTo(x => x.Request.To);
|
||||
RuleFor(x => x.Request)
|
||||
.Must(r => !string.IsNullOrWhiteSpace(r.BlobPath) || !string.IsNullOrWhiteSpace(r.ContentBase64))
|
||||
.WithMessage("Either BlobPath or ContentBase64 must be provided");
|
||||
When(x => !string.IsNullOrWhiteSpace(x.Request.ContentBase64), () =>
|
||||
{
|
||||
RuleFor(x => x.Request.ContentType).NotEmpty().MaximumLength(255);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
using MediatR;
|
||||
using MinAttest.Application.Abstractions;
|
||||
using MinAttest.Contracts.Attests;
|
||||
using MinAttest.Domain.Entities;
|
||||
|
||||
namespace MinAttest.Application.Features.Attests.Commands;
|
||||
|
||||
public record PersonUploadAttestCommand(Guid PersonId, PersonAttestUploadRequest Request) : IRequest<Guid?>;
|
||||
|
||||
public class PersonUploadAttestCommandHandler(IAppDbContext db) : IRequestHandler<PersonUploadAttestCommand, Guid?>
|
||||
{
|
||||
public async Task<Guid?> Handle(PersonUploadAttestCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var r = request.Request;
|
||||
|
||||
var person = await db.Persons.FindAsync([request.PersonId], cancellationToken);
|
||||
if (person is null) return null;
|
||||
|
||||
byte[]? content = null;
|
||||
long? length = null;
|
||||
if (!string.IsNullOrWhiteSpace(r.ContentBase64))
|
||||
{
|
||||
try { content = Convert.FromBase64String(r.ContentBase64); length = content.LongLength; }
|
||||
catch (FormatException) { return null; }
|
||||
}
|
||||
|
||||
var a = new Attest
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PersonId = request.PersonId,
|
||||
EmployerId = null,
|
||||
Title = r.Title,
|
||||
From = r.From,
|
||||
To = r.To,
|
||||
Summary = r.Summary,
|
||||
BlobPath = r.BlobPath,
|
||||
BlobHash = r.BlobHash,
|
||||
Content = content,
|
||||
ContentType = r.ContentType,
|
||||
ContentLength = length,
|
||||
IssuedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
db.Attests.Add(a);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return a.Id;
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace MinAttest.Application.Features.Attests.Commands;
|
||||
|
||||
public class PersonUploadAttestCommandValidator : AbstractValidator<PersonUploadAttestCommand>
|
||||
{
|
||||
public PersonUploadAttestCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.PersonId).NotEqual(Guid.Empty);
|
||||
RuleFor(x => x.Request.Title).NotEmpty().MaximumLength(200);
|
||||
RuleFor(x => x.Request.BlobPath).MaximumLength(500);
|
||||
RuleFor(x => x.Request.From).LessThanOrEqualTo(x => x.Request.To);
|
||||
RuleFor(x => x.Request)
|
||||
.Must(r => !string.IsNullOrWhiteSpace(r.BlobPath) || !string.IsNullOrWhiteSpace(r.ContentBase64))
|
||||
.WithMessage("Either BlobPath or ContentBase64 must be provided");
|
||||
When(x => !string.IsNullOrWhiteSpace(x.Request.ContentBase64), () =>
|
||||
{
|
||||
RuleFor(x => x.Request.ContentType).NotEmpty().MaximumLength(255);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MinAttest.Contracts.Attests;
|
||||
using MinAttest.Application.Abstractions;
|
||||
using MinAttest.Domain.Entities;
|
||||
|
||||
namespace MinAttest.Application.Features.Attests.Queries;
|
||||
|
||||
public record GetAttestByIdQuery(Guid Id) : IRequest<AttestDetails?>;
|
||||
|
||||
public class GetAttestByIdQueryHandler(IAppDbContext db) : IRequestHandler<GetAttestByIdQuery, AttestDetails?>
|
||||
{
|
||||
public async Task<AttestDetails?> Handle(GetAttestByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var a = await db.Attests.AsNoTracking().Include(x => x.Employer)
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
|
||||
if (a is null) return null;
|
||||
|
||||
return new AttestDetails(
|
||||
a.Id,
|
||||
a.Employer?.Name ?? string.Empty,
|
||||
a.Title,
|
||||
a.From,
|
||||
a.To,
|
||||
a.Summary ?? string.Empty,
|
||||
a.Status == AttestStatus.Issued
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MinAttest.Application.Abstractions;
|
||||
|
||||
namespace MinAttest.Application.Features.Attests.Queries;
|
||||
|
||||
public record GetAttestContentQuery(Guid AttestId) : IRequest<AttestContentResult?>;
|
||||
|
||||
public record AttestContentResult(byte[] Content, string? ContentType, string FileName);
|
||||
|
||||
public class GetAttestContentQueryHandler(IAppDbContext db) : IRequestHandler<GetAttestContentQuery, AttestContentResult?>
|
||||
{
|
||||
public async Task<AttestContentResult?> Handle(GetAttestContentQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await db.Attests.AsNoTracking()
|
||||
.Where(a => a.Id == request.AttestId)
|
||||
.Select(a => new { a.Content, a.ContentType, a.Title })
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (result is null || result.Content is null)
|
||||
return null;
|
||||
|
||||
var name = string.IsNullOrWhiteSpace(result.Title) ? "attest" : result.Title;
|
||||
// crude filename, no extension without content type mapping
|
||||
return new AttestContentResult(result.Content, result.ContentType, name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MinAttest.Contracts.Attests;
|
||||
using MinAttest.Domain.Entities;
|
||||
using MinAttest.Application.Abstractions;
|
||||
|
||||
namespace MinAttest.Application.Features.Attests.Queries;
|
||||
|
||||
public record GetAttestsQuery(Guid? PersonId, Guid? EmployerId, int? Take) : IRequest<IReadOnlyList<AttestSummary>>;
|
||||
|
||||
public class GetAttestsQueryHandler(IAppDbContext db) : IRequestHandler<GetAttestsQuery, IReadOnlyList<AttestSummary>>
|
||||
{
|
||||
public async Task<IReadOnlyList<AttestSummary>> Handle(GetAttestsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = db.Attests.AsNoTracking().Include(a => a.Employer).AsQueryable();
|
||||
|
||||
if (request.PersonId is Guid pid && pid != Guid.Empty)
|
||||
query = query.Where(a => a.PersonId == pid);
|
||||
if (request.EmployerId is Guid eid && eid != Guid.Empty)
|
||||
query = query.Where(a => a.EmployerId == eid);
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(a => a.IssuedAt)
|
||||
.Take(Math.Clamp(request.Take ?? 50, 1, 200))
|
||||
.Select(a => new AttestSummary(
|
||||
a.Id,
|
||||
a.Employer != null ? a.Employer.Name : string.Empty,
|
||||
a.Title,
|
||||
a.From,
|
||||
a.To,
|
||||
a.Status == AttestStatus.Issued
|
||||
))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MinAttest.Application.Abstractions;
|
||||
using MinAttest.Contracts.Attests;
|
||||
using MinAttest.Domain.Entities;
|
||||
|
||||
namespace MinAttest.Application.Features.Attests.Queries;
|
||||
|
||||
public record ListEmployerAttestsQuery(Guid EmployerId, int? Take) : IRequest<IReadOnlyList<AttestSummary>>;
|
||||
|
||||
public class ListEmployerAttestsQueryHandler(IAppDbContext db) : IRequestHandler<ListEmployerAttestsQuery, IReadOnlyList<AttestSummary>>
|
||||
{
|
||||
public async Task<IReadOnlyList<AttestSummary>> Handle(ListEmployerAttestsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await db.Attests.AsNoTracking().Include(a => a.Employer)
|
||||
.Where(a => a.EmployerId == request.EmployerId)
|
||||
.OrderByDescending(a => a.IssuedAt)
|
||||
.Take(Math.Clamp(request.Take ?? 50, 1, 200))
|
||||
.Select(a => new AttestSummary(a.Id, (a.Employer != null ? a.Employer.Name : string.Empty), a.Title, a.From, a.To, a.Status == AttestStatus.Issued))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MinAttest.Application.Abstractions;
|
||||
using MinAttest.Contracts.Attests;
|
||||
using MinAttest.Domain.Entities;
|
||||
|
||||
namespace MinAttest.Application.Features.Attests.Queries;
|
||||
|
||||
public record ListPersonAttestsQuery(Guid PersonId, int? Take) : IRequest<IReadOnlyList<AttestSummary>>;
|
||||
|
||||
public class ListPersonAttestsQueryHandler(IAppDbContext db) : IRequestHandler<ListPersonAttestsQuery, IReadOnlyList<AttestSummary>>
|
||||
{
|
||||
public async Task<IReadOnlyList<AttestSummary>> Handle(ListPersonAttestsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await db.Attests.AsNoTracking().Include(a => a.Employer)
|
||||
.Where(a => a.PersonId == request.PersonId)
|
||||
.OrderByDescending(a => a.IssuedAt)
|
||||
.Take(Math.Clamp(request.Take ?? 50, 1, 200))
|
||||
.Select(a => new AttestSummary(a.Id, (a.Employer != null ? a.Employer.Name : string.Empty), a.Title, a.From, a.To, a.Status == AttestStatus.Issued))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user