Skip to content

Commit

Permalink
Merge pull request #24 from shishnk/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
shishnk authored Apr 14, 2024
2 parents 78daa9a + b009ed7 commit 0ff9038
Show file tree
Hide file tree
Showing 149 changed files with 5,428 additions and 102 deletions.
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
POSTGRES_DB=weather-database
POSTGRES_USER=user
POSTGRES_PASSWORD=pass
TELEGRAM_BOT_TOKEN=6819090366:AAEp-IrmVXY-U2Ie91lZktlkjxPG1IkJTJU
33 changes: 33 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: .NET

on:
push:
branches: [ master, develop ]
pull_request:
branches: [ master, develop ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Build
run: dotnet build

tests:
needs: build
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Test WeatherApp
run: dotnet test ./WeatherBotApi.WeatherApp/Tests/WeatherApp.Tests/WeatherApp.Tests.csproj --verbosity normal

- name: Test DatabaseApp
run: dotnet test ./WeatherBotApi.DatabaseApp/Tests/DatabaseApp.Tests/DatabaseApp.Tests.csproj --verbosity normal
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,7 @@ MigrationBackup/
.ionide/

# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd

# idea
.idea/
15 changes: 0 additions & 15 deletions .idea/.idea.weather-bot-api/.idea/.gitignore

This file was deleted.

4 changes: 0 additions & 4 deletions .idea/.idea.weather-bot-api/.idea/encodings.xml

This file was deleted.

8 changes: 0 additions & 8 deletions .idea/.idea.weather-bot-api/.idea/indexLayout.xml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/.idea.weather-bot-api/.idea/vcs.xml

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using FluentResults;
using MediatR;
using Microsoft.Extensions.Logging;

namespace DatabaseApp.Application.Common.Behaviors;

// ReSharper disable once UnusedType.Global
public class RequestLoggingBehavior<TRequest, TResponse>(ILogger<TRequest> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
where TResponse : ResultBase
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var requestName = typeof(TRequest).Name;

logger.LogInformation("Handling {RequestName}: {@Request}", requestName, request);

var response = await next();

if (response.IsSuccess)
{
logger.LogInformation("{Request} handled successfully", requestName);
}
else
{
foreach (var error in response.Errors)
{
logger.LogError("Error: {Error}", error.Message);
}
}

return response;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using FluentValidation;
using MediatR;

namespace DatabaseApp.Application.Common.Behaviors;

public class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var context = new ValidationContext<TRequest>(request);
var failures = validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(failure => failure != null)
.ToList();

if (failures.Count != 0) throw new ValidationException(failures);

return await next();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using DatabaseApp.Application.Users;
using DatabaseApp.Application.UserWeatherSubscriptions;
using DatabaseApp.Domain.Models;
using Mapster;

namespace DatabaseApp.Application.Common.Mapping;

public class RegisterMapper : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<UserWeatherSubscription, UserWeatherSubscriptionDto>()
.Map(dest => dest.Location, src => src.Location.Value)
.Map(dest => dest.ResendInterval, src => src.ResendInterval)
.Map(dest => dest.UserTelegramId, src => src.User.TelegramId);
config.NewConfig<User, UserDto>()
.Map(dest => dest.TelegramId, src => src.TelegramId)
.Map(dest => dest.MobileNumber, src => src.Metadata.MobileNumber)
.Map(dest => dest.Username, src => src.Metadata.Username)
.Map(dest => dest.RegisteredAt, src => src.RegisteredAt);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
<PackageReference Include="Mapster" Version="7.4.1-pre01"/>
<PackageReference Include="Mapster.Async" Version="2.0.2-pre01"/>
<PackageReference Include="Mapster.Core" Version="1.2.2-pre01"/>
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.2-pre01"/>
<PackageReference Include="Mapster.EFCore" Version="5.1.2-pre01"/>
<PackageReference Include="MediatR" Version="12.2.0"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DatabaseApp.Domain\DatabaseApp.Domain.csproj"/>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Reflection;
using DatabaseApp.Application.Common.Behaviors;
using DatabaseApp.Application.Common.Mapping;
using DatabaseApp.Application.Users.Queries.GetAllUsers;
using FluentValidation;
using Mapster;
using MapsterMapper;
using MediatR;
using Microsoft.Extensions.DependencyInjection;

namespace DatabaseApp.Application;

public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
var config = new TypeAdapterConfig();
new RegisterMapper().Register(config);

services.AddSingleton(config);
services.AddScoped<IMapper, ServiceMapper>();
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
cfg.AddOpenBehavior(typeof(RequestLoggingBehavior<,>));
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
});
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using FluentResults;
using MediatR;

namespace DatabaseApp.Application.UserWeatherSubscriptions.Commands.CreateUserWeatherSubscription;

public class CreateUserWeatherSubscriptionCommand : IRequest<Result>
{
public long TelegramUserId { get; init; }
public required string Location { get; init; }
public TimeSpan ResendInterval { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using DatabaseApp.Domain.Models;
using DatabaseApp.Domain.Repositories;
using FluentResults;
using MediatR;

namespace DatabaseApp.Application.UserWeatherSubscriptions.Commands.CreateUserWeatherSubscription;

// ReSharper disable once UnusedType.Global
public class CreateUserWeatherSubscriptionCommandHandler(IUnitOfWork unitOfWork)
: IRequestHandler<CreateUserWeatherSubscriptionCommand, Result>
{
public async Task<Result> Handle(CreateUserWeatherSubscriptionCommand request, CancellationToken cancellationToken)
{
var location = Location.Create(request.Location);

if (location.IsFailed) return location.ToResult();

var existingSubscription = await unitOfWork.UserWeatherSubscriptionRepository
.GetByUserTelegramIdAndLocationAsync(request.TelegramUserId, location.Value, cancellationToken);

if (existingSubscription != null) return Result.Fail(new Error("Weather subscription already exists"));

var user = await unitOfWork.UserRepository.GetByTelegramIdAsync(request.TelegramUserId, cancellationToken);

if (user == null) return Result.Fail(new Error("User not found"));

var weatherSubscription = new UserWeatherSubscription
{
UserId = user.Id,
Location = location.Value,
ResendInterval = request.ResendInterval
};

await unitOfWork.UserWeatherSubscriptionRepository.AddAsync(weatherSubscription, cancellationToken);
await unitOfWork.SaveDbChangesAsync(cancellationToken);

return Result.Ok();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using FluentValidation;

namespace DatabaseApp.Application.UserWeatherSubscriptions.Commands.CreateUserWeatherSubscription;

// ReSharper disable once UnusedType.Global
public class CreateUserWeatherSubscriptionCommandValidator : AbstractValidator<CreateUserWeatherSubscriptionCommand>
{
public CreateUserWeatherSubscriptionCommandValidator()
{
RuleFor(x => x.TelegramUserId).GreaterThan(0);
RuleFor(x => x.Location).NotNull().NotEmpty();
RuleFor(x => x.ResendInterval).GreaterThan(TimeSpan.Zero);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using FluentResults;
using MediatR;

namespace DatabaseApp.Application.UserWeatherSubscriptions.Commands.DeleteUserWeatherSubscription;

public class DeleteUserWeatherSubscriptionCommand : IRequest<Result>
{
public long UserTelegramId { get; init; }
public required string Location { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using DatabaseApp.Domain.Models;
using DatabaseApp.Domain.Repositories;
using FluentResults;
using MediatR;

namespace DatabaseApp.Application.UserWeatherSubscriptions.Commands.DeleteUserWeatherSubscription;

// ReSharper disable once UnusedType.Global
public class DeleteUserWeatherSubscriptionCommandHandler(IWeatherSubscriptionRepository repository)
: IRequestHandler<DeleteUserWeatherSubscriptionCommand, Result>
{
// ReSharper disable once UnusedMember.Global
public async Task<Result> Handle(DeleteUserWeatherSubscriptionCommand request, CancellationToken cancellationToken)
{
var location = Location.Create(request.Location);

if (location.IsFailed) return location.ToResult();

var subscription =
await repository.GetByUserTelegramIdAndLocationAsync(request.UserTelegramId, location.Value,
cancellationToken);

if (subscription == null) return Result.Fail(new Error("Subscription not found"));

repository.Delete(subscription);
await repository.SaveDbChangesAsync(cancellationToken);

return Result.Ok();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using FluentValidation;

namespace DatabaseApp.Application.UserWeatherSubscriptions.Commands.DeleteUserWeatherSubscription;

// ReSharper disable once UnusedType.Global
public class DeleteUserWeatherSubscriptionCommandValidator : AbstractValidator<DeleteUserWeatherSubscriptionCommand>
{
public DeleteUserWeatherSubscriptionCommandValidator()
{
RuleFor(x => x.UserTelegramId).GreaterThan(0);
RuleFor(x => x.Location).NotNull().NotEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using FluentResults;
using MediatR;

namespace DatabaseApp.Application.UserWeatherSubscriptions.Commands.UpdateUserWeatherSubscription;

public class UpdateUserWeatherSubscriptionCommand : IRequest<Result>
{
public long UserTelegramId { get; init; }
public required string Location { get; init; }
public TimeSpan ResendInterval { get; init; }
}
Loading

0 comments on commit 0ff9038

Please sign in to comment.