feat: add "Popular stations" statistic

This commit is contained in:
cuqmbr 2022-11-27 16:08:33 +02:00
parent 3d6ffa25ab
commit 1de4e24f22
15 changed files with 1243 additions and 58 deletions

View File

@ -54,9 +54,18 @@ public class StatisticsController : ControllerBase
}
[HttpGet("stations")]
public async Task<IActionResult> GetPopularStations([FromQuery] int amount = 10)
public async Task<IActionResult> GetPopularStations([FromQuery] PopularAddressesParameters parameters)
{
return Ok();
var result = await _statisticsService.GetPopularStations(parameters);
if (!result.IsSucceed)
{
return BadRequest(result.message);
}
Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(result.pagingMetadata));
return Ok(result.stations);
}
}

View File

@ -21,6 +21,7 @@ public class ApplicationDbContext : IdentityDbContext<User>
public DbSet<City> Cities { get; set; } = null!;
public DbSet<State> States { get; set; } = null!;
public DbSet<Country> Countries { get; set; } = null!;
public DbSet<TicketGroup> TicketGroups { get; set; } = null!;
public DbSet<Ticket> Tickets { get; set; } = null!;
public DbSet<Review> Reviews { get; set; } = null!;
}

View File

@ -0,0 +1,860 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Server.Data;
#nullable disable
namespace Server.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20221124184433_Add_TicketGroups")]
partial class Add_TicketGroups
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Server.Models.Address", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("CityId")
.HasColumnType("integer");
b.Property<double>("Latitude")
.HasColumnType("double precision");
b.Property<double>("Longitude")
.HasColumnType("double precision");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CityId");
b.ToTable("Addresses");
});
modelBuilder.Entity("Server.Models.City", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("StateId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("StateId");
b.ToTable("Cities");
});
modelBuilder.Entity("Server.Models.Company", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("OwnerId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.ToTable("Companies");
});
modelBuilder.Entity("Server.Models.Country", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Code")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Countries");
});
modelBuilder.Entity("Server.Models.Review", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Comment")
.HasColumnType("text");
b.Property<DateTime>("PostDateTimeUtc")
.HasColumnType("timestamp with time zone");
b.Property<int>("Rating")
.HasColumnType("integer");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("VehicleEnrollmentId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("VehicleEnrollmentId");
b.ToTable("Reviews");
});
modelBuilder.Entity("Server.Models.Route", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Routes");
});
modelBuilder.Entity("Server.Models.RouteAddress", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("AddressId")
.HasColumnType("integer");
b.Property<double>("CostToNextCity")
.HasColumnType("double precision");
b.Property<int>("Order")
.HasColumnType("integer");
b.Property<int>("RouteId")
.HasColumnType("integer");
b.Property<TimeSpan>("TimeSpanToNextCity")
.HasColumnType("interval");
b.Property<TimeSpan>("WaitTimeSpan")
.HasColumnType("interval");
b.HasKey("Id");
b.HasIndex("AddressId");
b.HasIndex("RouteId");
b.ToTable("RouteAddresses");
});
modelBuilder.Entity("Server.Models.State", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("CountryId")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CountryId");
b.ToTable("States");
});
modelBuilder.Entity("Server.Models.Ticket", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("FirstRouteAddressId")
.HasColumnType("integer");
b.Property<bool>("IsMissed")
.HasColumnType("boolean");
b.Property<bool>("IsReturned")
.HasColumnType("boolean");
b.Property<int>("LastRouteAddressId")
.HasColumnType("integer");
b.Property<DateTime>("PurchaseDateTimeUtc")
.HasColumnType("timestamp with time zone");
b.Property<int>("TicketGroupId")
.HasColumnType("integer");
b.Property<int>("VehicleEnrollmentId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("TicketGroupId");
b.HasIndex("VehicleEnrollmentId");
b.ToTable("Tickets");
});
modelBuilder.Entity("Server.Models.TicketGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("TicketGroups");
});
modelBuilder.Entity("Server.Models.User", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<string>("FirstName")
.HasColumnType("text");
b.Property<string>("LastName")
.HasColumnType("text");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Server.Models.Vehicle", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Capacity")
.HasColumnType("integer");
b.Property<int>("CompanyId")
.HasColumnType("integer");
b.Property<bool>("HasBelts")
.HasColumnType("boolean");
b.Property<bool>("HasClimateControl")
.HasColumnType("boolean");
b.Property<bool>("HasOutlet")
.HasColumnType("boolean");
b.Property<bool>("HasStewardess")
.HasColumnType("boolean");
b.Property<bool>("HasTV")
.HasColumnType("boolean");
b.Property<bool>("HasWC")
.HasColumnType("boolean");
b.Property<bool>("HasWiFi")
.HasColumnType("boolean");
b.Property<string>("Number")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CompanyId");
b.ToTable("Vehicles");
});
modelBuilder.Entity("Server.Models.VehicleEnrollment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("CancelationComment")
.HasColumnType("text");
b.Property<TimeSpan?>("DelayTimeSpan")
.HasColumnType("interval");
b.Property<DateTime>("DepartureDateTimeUtc")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsCanceled")
.HasColumnType("boolean");
b.Property<int>("RouteId")
.HasColumnType("integer");
b.Property<int>("VehicleId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("RouteId");
b.HasIndex("VehicleId");
b.ToTable("VehicleEnrollments");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Server.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Server.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Server.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Server.Models.Address", b =>
{
b.HasOne("Server.Models.City", "City")
.WithMany("Addresses")
.HasForeignKey("CityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("City");
});
modelBuilder.Entity("Server.Models.City", b =>
{
b.HasOne("Server.Models.State", "State")
.WithMany("Cities")
.HasForeignKey("StateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("State");
});
modelBuilder.Entity("Server.Models.Company", b =>
{
b.HasOne("Server.Models.User", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
});
modelBuilder.Entity("Server.Models.Review", b =>
{
b.HasOne("Server.Models.User", "User")
.WithMany("Reviews")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.Models.VehicleEnrollment", "VehicleEnrollment")
.WithMany("Reviews")
.HasForeignKey("VehicleEnrollmentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
b.Navigation("VehicleEnrollment");
});
modelBuilder.Entity("Server.Models.RouteAddress", b =>
{
b.HasOne("Server.Models.Address", "Address")
.WithMany("RouteAddresses")
.HasForeignKey("AddressId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.Models.Route", "Route")
.WithMany("RouteAddresses")
.HasForeignKey("RouteId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Address");
b.Navigation("Route");
});
modelBuilder.Entity("Server.Models.State", b =>
{
b.HasOne("Server.Models.Country", "Country")
.WithMany("States")
.HasForeignKey("CountryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Country");
});
modelBuilder.Entity("Server.Models.Ticket", b =>
{
b.HasOne("Server.Models.TicketGroup", "TicketGroup")
.WithMany("Tickets")
.HasForeignKey("TicketGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.Models.VehicleEnrollment", "VehicleEnrollment")
.WithMany("Tickets")
.HasForeignKey("VehicleEnrollmentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("TicketGroup");
b.Navigation("VehicleEnrollment");
});
modelBuilder.Entity("Server.Models.TicketGroup", b =>
{
b.HasOne("Server.Models.User", "User")
.WithMany("TicketGroups")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Server.Models.User", b =>
{
b.OwnsMany("Server.Models.RefreshToken", "RefreshTokens", b1 =>
{
b1.Property<string>("UserId")
.HasColumnType("text");
b1.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
b1.Property<DateTime>("CreationDateTime")
.HasColumnType("timestamp with time zone");
b1.Property<DateTime>("ExpiryDateTime")
.HasColumnType("timestamp with time zone");
b1.Property<DateTime?>("Revoked")
.HasColumnType("timestamp with time zone");
b1.Property<string>("Token")
.IsRequired()
.HasColumnType("text");
b1.HasKey("UserId", "Id");
b1.ToTable("RefreshToken");
b1.WithOwner()
.HasForeignKey("UserId");
});
b.Navigation("RefreshTokens");
});
modelBuilder.Entity("Server.Models.Vehicle", b =>
{
b.HasOne("Server.Models.Company", "Company")
.WithMany("Vehicles")
.HasForeignKey("CompanyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Company");
});
modelBuilder.Entity("Server.Models.VehicleEnrollment", b =>
{
b.HasOne("Server.Models.Route", "Route")
.WithMany("VehicleEnrollments")
.HasForeignKey("RouteId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.Models.Vehicle", "Vehicle")
.WithMany("VehicleEnrollments")
.HasForeignKey("VehicleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Route");
b.Navigation("Vehicle");
});
modelBuilder.Entity("Server.Models.Address", b =>
{
b.Navigation("RouteAddresses");
});
modelBuilder.Entity("Server.Models.City", b =>
{
b.Navigation("Addresses");
});
modelBuilder.Entity("Server.Models.Company", b =>
{
b.Navigation("Vehicles");
});
modelBuilder.Entity("Server.Models.Country", b =>
{
b.Navigation("States");
});
modelBuilder.Entity("Server.Models.Route", b =>
{
b.Navigation("RouteAddresses");
b.Navigation("VehicleEnrollments");
});
modelBuilder.Entity("Server.Models.State", b =>
{
b.Navigation("Cities");
});
modelBuilder.Entity("Server.Models.TicketGroup", b =>
{
b.Navigation("Tickets");
});
modelBuilder.Entity("Server.Models.User", b =>
{
b.Navigation("Reviews");
b.Navigation("TicketGroups");
});
modelBuilder.Entity("Server.Models.Vehicle", b =>
{
b.Navigation("VehicleEnrollments");
});
modelBuilder.Entity("Server.Models.VehicleEnrollment", b =>
{
b.Navigation("Reviews");
b.Navigation("Tickets");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,129 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Server.Migrations
{
public partial class Add_TicketGroups : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Tickets_AspNetUsers_UserId",
table: "Tickets");
migrationBuilder.DropIndex(
name: "IX_Tickets_UserId",
table: "Tickets");
migrationBuilder.DropColumn(
name: "UserId",
table: "Tickets");
migrationBuilder.AddColumn<int>(
name: "FirstRouteAddressId",
table: "Tickets",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "LastRouteAddressId",
table: "Tickets",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "TicketGroupId",
table: "Tickets",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "TicketGroups",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TicketGroups", x => x.Id);
table.ForeignKey(
name: "FK_TicketGroups_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Tickets_TicketGroupId",
table: "Tickets",
column: "TicketGroupId");
migrationBuilder.CreateIndex(
name: "IX_TicketGroups_UserId",
table: "TicketGroups",
column: "UserId");
migrationBuilder.AddForeignKey(
name: "FK_Tickets_TicketGroups_TicketGroupId",
table: "Tickets",
column: "TicketGroupId",
principalTable: "TicketGroups",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Tickets_TicketGroups_TicketGroupId",
table: "Tickets");
migrationBuilder.DropTable(
name: "TicketGroups");
migrationBuilder.DropIndex(
name: "IX_Tickets_TicketGroupId",
table: "Tickets");
migrationBuilder.DropColumn(
name: "FirstRouteAddressId",
table: "Tickets");
migrationBuilder.DropColumn(
name: "LastRouteAddressId",
table: "Tickets");
migrationBuilder.DropColumn(
name: "TicketGroupId",
table: "Tickets");
migrationBuilder.AddColumn<string>(
name: "UserId",
table: "Tickets",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.CreateIndex(
name: "IX_Tickets_UserId",
table: "Tickets",
column: "UserId");
migrationBuilder.AddForeignKey(
name: "FK_Tickets_AspNetUsers_UserId",
table: "Tickets",
column: "UserId",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@ -363,31 +363,55 @@ namespace Server.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("FirstRouteAddressId")
.HasColumnType("integer");
b.Property<bool>("IsMissed")
.HasColumnType("boolean");
b.Property<bool>("IsReturned")
.HasColumnType("boolean");
b.Property<int>("LastRouteAddressId")
.HasColumnType("integer");
b.Property<DateTime>("PurchaseDateTimeUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("TicketGroupId")
.HasColumnType("integer");
b.Property<int>("VehicleEnrollmentId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("TicketGroupId");
b.HasIndex("VehicleEnrollmentId");
b.ToTable("Tickets");
});
modelBuilder.Entity("Server.Models.TicketGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("TicketGroups");
});
modelBuilder.Entity("Server.Models.User", b =>
{
b.Property<string>("Id")
@ -678,9 +702,9 @@ namespace Server.Migrations
modelBuilder.Entity("Server.Models.Ticket", b =>
{
b.HasOne("Server.Models.User", "User")
b.HasOne("Server.Models.TicketGroup", "TicketGroup")
.WithMany("Tickets")
.HasForeignKey("UserId")
.HasForeignKey("TicketGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -690,11 +714,22 @@ namespace Server.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
b.Navigation("TicketGroup");
b.Navigation("VehicleEnrollment");
});
modelBuilder.Entity("Server.Models.TicketGroup", b =>
{
b.HasOne("Server.Models.User", "User")
.WithMany("TicketGroups")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Server.Models.User", b =>
{
b.OwnsMany("Server.Models.RefreshToken", "RefreshTokens", b1 =>
@ -794,11 +829,16 @@ namespace Server.Migrations
b.Navigation("Cities");
});
modelBuilder.Entity("Server.Models.TicketGroup", b =>
{
b.Navigation("Tickets");
});
modelBuilder.Entity("Server.Models.User", b =>
{
b.Navigation("Reviews");
b.Navigation("Tickets");
b.Navigation("TicketGroups");
});
modelBuilder.Entity("Server.Models.Vehicle", b =>

View File

@ -9,14 +9,16 @@ public class Ticket
public int Id { get; set; }
[ForeignKey("UserId")]
public string UserId { get; set; } = null!;
public User User { get; set; } = null!;
public int TicketGroupId { get; set; }
public TicketGroup TicketGroup { get; set; } = null!;
[ForeignKey("VehicleEnrollmentId")]
public int VehicleEnrollmentId { get; set; }
public VehicleEnrollment VehicleEnrollment { get; set; } = null!;
public DateTime PurchaseDateTimeUtc { get; set; } = DateTime.UtcNow;
public int FirstRouteAddressId { get; set; }
public int LastRouteAddressId { get; set; }
public bool IsReturned { get; set; } = false;
public bool IsMissed { get; set; } = false;
}

View File

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Server.Models;
public class TicketGroup
{
[Key]
public int Id { get; set; }
public string UserId { get; set; } = null!;
public User User { get; set; } = null!;
public virtual IList<Ticket> Tickets { get; set; }
}

View File

@ -7,6 +7,6 @@ public class User : IdentityUser
public string? FirstName { get; set; }
public string? LastName { get; set; }
public IList<RefreshToken> RefreshTokens { get; set; } = null!;
public virtual IList<Ticket> Tickets { get; set; } = null!;
public virtual IList<TicketGroup> TicketGroups { get; set; } = null!;
public virtual IList<Review> Reviews { get; set; } = null!;
}

View File

@ -134,6 +134,7 @@ builder.Services.AddScoped<IDataShaper<RouteAddress>, DataShaper<RouteAddress>>(
builder.Services.AddScoped<IDataShaper<UserDto>, DataShaper<UserDto>>();
builder.Services.AddScoped<IDataShaper<CompanyDto>, DataShaper<CompanyDto>>();
builder.Services.AddScoped<IDataShaper<AddressDto>, DataShaper<AddressDto>>();
// Adding DB Context with PostgreSQL
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

View File

@ -10,12 +10,15 @@ public interface IStatisticsService
Task<(bool IsSucceed, string? message, IEnumerable<ExpandoObject> route)>
GetPopularRoutes(int amount);
Task<(bool IsSucceed, string? message, IEnumerable<ExpandoObject> users, PagingMetadata<User> pagingMetadata)>
Task<(bool IsSucceed, string? message, IEnumerable<ExpandoObject> users,
PagingMetadata<ExpandoObject> pagingMetadata)>
GetEngagedUsers(EngagedUserParameters parameters);
Task<(bool IsSucceed, string? message, IEnumerable<ExpandoObject> companies, PagingMetadata<Company> pagingMetadata)>
Task<(bool IsSucceed, string? message, IEnumerable<ExpandoObject> companies,
PagingMetadata<ExpandoObject> pagingMetadata)>
GetPopularCompanies(PopularCompanyParameters parameters);
Task<(bool IsSucceed, string? message, IEnumerable<ExpandoObject> stations)>
GetPopularStations(int amount);
Task<(bool IsSucceed, string? message, IEnumerable<ExpandoObject> stations,
PagingMetadata<ExpandoObject> pagingMetadata)>
GetPopularStations(PopularAddressesParameters parameters);
}

View File

@ -16,14 +16,17 @@ public class StatisticsService : IStatisticsService
private readonly IMapper _mapper;
private readonly IDataShaper<UserDto> _userDataShaper;
private readonly IDataShaper<CompanyDto> _companyDataShaper;
private readonly IDataShaper<AddressDto> _addressDataShaper;
public StatisticsService(ApplicationDbContext dbContext, IMapper mapper,
IDataShaper<UserDto> userDataShaper, IDataShaper<CompanyDto> companyDataShaper)
IDataShaper<UserDto> userDataShaper, IDataShaper<CompanyDto> companyDataShaper,
IDataShaper<AddressDto> addressDataShaper)
{
_dbContext = dbContext;
_mapper = mapper;
_userDataShaper = userDataShaper;
_companyDataShaper = companyDataShaper;
_addressDataShaper = addressDataShaper;
}
// Popularity is measured in number of purchased tickets
@ -33,48 +36,56 @@ public class StatisticsService : IStatisticsService
throw new NotImplementedException();
}
// Engagement is measured in number of tickets bought in last 60 days
public async Task<(bool IsSucceed, string? message, IEnumerable<ExpandoObject> users, PagingMetadata<User> pagingMetadata)>
// Engagement is measured in number of purchases made in past N days
// One purchase contains one (direct route) or more (route with transfers) tickets
public async Task<(bool IsSucceed, string? message, IEnumerable<ExpandoObject> users,
PagingMetadata<ExpandoObject> pagingMetadata)>
GetEngagedUsers(EngagedUserParameters parameters)
{
var fromDateUtc = DateTime.UtcNow - TimeSpan.FromDays(parameters.Days ?? parameters.DefaultDays);
var fromDateUtc =
DateTime.UtcNow - TimeSpan.FromDays(parameters.Days ?? parameters.DefaultDays);
var resultObjects = _dbContext.Users
.Include(u => u.Tickets)
.Include(u => u.TicketGroups)
.ThenInclude(tg => tg.Tickets)
.Select(u => new
{
User = u,
Tickets = u.Tickets.Where(t => t.PurchaseDateTimeUtc >= fromDateUtc)
TicketGroups = u.TicketGroups.Where(tg =>
tg.Tickets.First().PurchaseDateTimeUtc >= fromDateUtc)
})
.OrderByDescending(o => o.User.Tickets.Count)
.OrderByDescending(o => o.TicketGroups.Count())
.Take(parameters.Amount);
var dbUsers = resultObjects.Select(i => i.User);
var pagingMetadata = ApplyPaging(ref dbUsers, parameters.PageNumber,
parameters.PageSize);
var userDtos = _mapper.ProjectTo<UserDto>(dbUsers).ToArray();
var shapedData = _userDataShaper
var shapedDataArray = _userDataShaper
.ShapeData(userDtos, parameters.Fields ?? parameters.DefaultFields)
.ToArray();
if (parameters.Fields != null &&
parameters.Fields.ToLower().Contains("ticketCount".ToLower()))
parameters.Fields.ToLower().Contains("purchaseCount".ToLower()))
{
var dbUsersArray = await dbUsers.ToArrayAsync();
for (int i = 0; i < dbUsersArray.Length; i++)
{
var ticketCount = dbUsersArray[i].Tickets.Count;
shapedData[i].TryAdd("TicketCount", ticketCount);
var ticketCount = dbUsersArray[i].TicketGroups.Count;
shapedDataArray[i].TryAdd("PurchaseCount", ticketCount);
}
}
return (true, null, shapedData, pagingMetadata);
var shapedData = shapedDataArray.AsQueryable();
var pagingMetadata = ApplyPaging(ref shapedData, parameters.PageNumber,
parameters.PageSize);
shapedDataArray = shapedData.ToArray();
return (true, null, shapedDataArray, pagingMetadata);
}
// Popularity is measured in average rating of all VehicleEnrollments of a company
public async Task<(bool IsSucceed, string? message, IEnumerable<ExpandoObject> companies, PagingMetadata<Company> pagingMetadata)>
public async Task<(bool IsSucceed, string? message, IEnumerable<ExpandoObject> companies,
PagingMetadata<ExpandoObject> pagingMetadata)>
GetPopularCompanies(PopularCompanyParameters parameters)
{
var dbCompanies = _dbContext.Companies
@ -132,33 +143,133 @@ public class StatisticsService : IStatisticsService
}
}
companiesAvgRatings = companiesAvgRatings.Skip(companiesAvgRatings.Length - parameters.Amount).Reverse().ToArray();
var popularCompanies = dbCompaniesArray.Skip(companiesAvgRatings.Length - parameters.Amount).Reverse().AsQueryable();
companiesAvgRatings = companiesAvgRatings
.Skip(companiesAvgRatings.Length - parameters.Amount).Reverse()
.ToArray();
var popularCompanies = dbCompaniesArray
.Skip(companiesAvgRatings.Length - parameters.Amount).Reverse()
.AsQueryable();
// Apply paging, convert to DTOs and shape data
var pagingMetadata = ApplyPaging(ref popularCompanies, parameters.PageNumber, parameters.PageSize);
var companyDtos = _mapper.ProjectTo<CompanyDto>(popularCompanies).ToArray();
// Convert to DTOs, shape data and apply paging
var shapedData = _companyDataShaper.ShapeData(companyDtos, parameters.Fields ?? parameters.DefaultFields).ToArray();
var companyDtos = _mapper.ProjectTo<CompanyDto>(popularCompanies);
var shapedDataArray = _companyDataShaper.ShapeData(companyDtos,
parameters.Fields ?? parameters.DefaultFields).ToArray();
if (parameters.Fields != null &&
parameters.Fields.ToLower().Contains("rating".ToLower()))
{
for (int i = 0; i < shapedData.Length; i++)
for (int i = 0; i < shapedDataArray.Length; i++)
{
shapedData[i].TryAdd("Rating", companiesAvgRatings[i]);
shapedDataArray[i].TryAdd("Rating", companiesAvgRatings[i]);
}
}
return (true, null, shapedData, pagingMetadata);
var shapedData = shapedDataArray.AsQueryable();
var pagingMetadata = ApplyPaging(ref shapedData, parameters.PageNumber,
parameters.PageSize);
shapedDataArray = shapedData.ToArray();
return (true, null, shapedDataArray, pagingMetadata);
}
// Popularity is measured in number of routes using the station
public async Task<(bool IsSucceed, string? message, IEnumerable<ExpandoObject> stations)>
GetPopularStations(int amount)
// Popularity is measured in number tickets in which the address is the first or last station
public async Task<(bool IsSucceed, string? message, IEnumerable<ExpandoObject> stations,
PagingMetadata<ExpandoObject> pagingMetadata)>
GetPopularStations(PopularAddressesParameters parameters)
{
throw new NotImplementedException();
// throw new NotImplementedException();
var fromDateUtc =
DateTime.UtcNow - TimeSpan.FromDays(parameters.Days ?? parameters.DefaultDays);
var dbTicketGroupsArray = await _dbContext.TicketGroups
.Include(tg => tg.Tickets)
.Where(tg => tg.Tickets.First().PurchaseDateTimeUtc >= fromDateUtc)
.ToArrayAsync();
// Count appearances for each address id <Id, Count>
var addressCountDict = new Dictionary<int, int>();
foreach (var tg in dbTicketGroupsArray)
{
if (!addressCountDict.ContainsKey(tg.Tickets.First().FirstRouteAddressId))
{
addressCountDict.Add(tg.Tickets.First().FirstRouteAddressId, 1);
}
else
{
addressCountDict[tg.Tickets.First().FirstRouteAddressId] += 1;
}
if (!addressCountDict.ContainsKey(tg.Tickets.Last().LastRouteAddressId))
{
addressCountDict.Add(tg.Tickets.Last().LastRouteAddressId, 1);
}
else
{
addressCountDict[tg.Tickets.Last().LastRouteAddressId] += 1;
}
}
// Sort by number of appearances in descending order ->
// Take amount given in parameters ->
// Order by Id in Ascending order (needed for further sorting of two arrays simultaneously)
addressCountDict = addressCountDict.OrderByDescending(a => a.Value)
.Take(parameters.Amount).OrderBy(a => a.Key)
.ToDictionary(x => x.Key, x => x.Value);
// Separate Ids and counts into two arrays
var addressIds = addressCountDict.Keys.ToArray();
var addressCountArray = addressCountDict.Values.ToArray();
// Get top addresses from database ordered by Id (same as
// addressIds addressCountDict and )
var dbAddressesArray = await _dbContext.Addresses
.Where(a => addressIds.Any(id => a.Id == id))
.OrderBy(a => a.Id).ToArrayAsync();
// Sort addressCountArray and simultaneously sort dbAddressesArray
// in the same manner
int n = addressCountArray.Length;
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
if (addressCountArray[j] > addressCountArray[j + 1])
{
// swap temp and arr[i]
(addressCountArray[j], addressCountArray[j + 1]) = (addressCountArray[j + 1], addressCountArray[j]);
(dbAddressesArray[j], dbAddressesArray[j + 1]) = (dbAddressesArray[j + 1], dbAddressesArray[j]);
}
}
}
// Reverse sorted arrays (the result will be two "linked" arrays sorterd
// in descending order by addressCount)
addressCountArray = addressCountArray.Reverse().ToArray();
dbAddressesArray = dbAddressesArray.Reverse().ToArray();
var addressDtos =
_mapper.ProjectTo<AddressDto>(dbAddressesArray.AsQueryable());
var shapedDataArray = _addressDataShaper.ShapeData(addressDtos,
parameters.Fields ?? parameters.DefaultFields).ToArray();
if (parameters.Fields != null &&
parameters.Fields.ToLower().Contains("purchaseCount".ToLower()))
{
for (int i = 0; i < shapedDataArray.Length; i++)
{
shapedDataArray[i].TryAdd("purchaseCount", addressCountArray[i]);
}
}
var shapedData = shapedDataArray.AsQueryable();
var pagingMetadata = ApplyPaging(ref shapedData, parameters.PageNumber,
parameters.PageSize);
shapedDataArray = shapedData.ToArray();
return (true, null, shapedDataArray, pagingMetadata);
}
PagingMetadata<T> ApplyPaging<T>(ref IQueryable<T> obj,

View File

@ -92,16 +92,17 @@ public class TicketManagementService : ITicketManagementService
tickets = tickets.Where(t => t.IsReturned == isReturned);
}
// TODO: change TicketParameters
void FilterByTicketUserId(ref IQueryable<Ticket> tickets,
string? userId)
{
if (!tickets.Any() || String.IsNullOrWhiteSpace(userId))
{
return;
}
tickets = tickets.Where(t =>
t.UserId.ToLower().Contains(userId.ToLower()));
// if (!tickets.Any() || String.IsNullOrWhiteSpace(userId))
// {
// return;
// }
//
// tickets = tickets.Where(t =>
// t.UserId.ToLower().Contains(userId.ToLower()));
}
PagingMetadata<Ticket> ApplyPaging(ref IQueryable<Ticket> tickets,

View File

@ -10,7 +10,7 @@ public class PagingMetadata<T>
public bool HasPrevious => CurrentPage > 1;
public bool HasNext => CurrentPage < TotalPages;
public PagingMetadata(IQueryable<T> source, int pageNumber, int pageSize)
public PagingMetadata(IEnumerable<T> source, int pageNumber, int pageSize)
{
TotalCount = source.Count();
PageSize = pageSize;

View File

@ -2,7 +2,7 @@ namespace SharedModels.QueryParameters.Statistics;
public class EngagedUserParameters : ParametersBase
{
public readonly string DefaultFields = "id,firstName,lastName,username,email,phoneNumber,ticketCount";
public readonly string DefaultFields = "id,firstName,lastName,username,email,phoneNumber,purchaseCount";
public readonly int DefaultDays = 60;
public EngagedUserParameters()

View File

@ -0,0 +1,14 @@
namespace SharedModels.QueryParameters.Statistics;
public class PopularAddressesParameters : ParametersBase
{
public readonly string DefaultFields = "id,name,purchaseCount";
public readonly int DefaultDays = 60;
public PopularAddressesParameters()
{
Fields = DefaultFields;
}
public int? Days { get; set; }
}