Compare commits
17 changed files with 294 additions and 756 deletions
|
|
@ -31,7 +31,6 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Clone yavsc
|
- name: Clone yavsc
|
||||||
run: |
|
run: |
|
||||||
set -e
|
|
||||||
cd /src
|
cd /src
|
||||||
git clone https://forgejo.pschneider.fr/notazof/yavsc.git _src
|
git clone https://forgejo.pschneider.fr/notazof/yavsc.git _src
|
||||||
cd _src
|
cd _src
|
||||||
|
|
@ -45,10 +44,12 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
echo "🔍 Scanning for secrets..."
|
echo "🔍 Scanning for secrets..."
|
||||||
cd /src/_src && dotnet tool restore && dotnet picket git --verbose --redact --exit-code 1 --log-opts -n1 \
|
cd /src/_src && dotnet tool restore && dotnet picket git --verbose --redact --exit-code 1 --log-opts -n1 \
|
||||||
|
&& echo "✅ Success !" || echo "❌ Fail ($?)!"
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
echo "🚀 Lancement des tests..."
|
echo "🚀 Lancement des tests..."
|
||||||
cd /src/_src && dotnet test \
|
cd /src/_src && dotnet test \
|
||||||
--verbosity normal \
|
--verbosity normal \
|
||||||
--filter="Category!=Platform-Android" \
|
--filter="Category!=Platform-Android" \
|
||||||
--logger "xunit;LogFileName=test-results.xml"
|
--logger "xunit;LogFileName=test-results.xml" \
|
||||||
|
&& echo "✅ Success !" || { echo "❌ Fail ($?)!"; exit 1; }
|
||||||
|
|
|
||||||
22
.vscode/tasks.json
vendored
22
.vscode/tasks.json
vendored
|
|
@ -66,28 +66,6 @@
|
||||||
"isDefault": false
|
"isDefault": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"label": "test api backend (npgsql)",
|
|
||||||
"type": "process",
|
|
||||||
"problemMatcher": ["$msCompile"],
|
|
||||||
"command": "dotnet",
|
|
||||||
"args": [
|
|
||||||
"test",
|
|
||||||
"Yavsc.Api.Test.csproj",
|
|
||||||
"-v",
|
|
||||||
"minimal"
|
|
||||||
],
|
|
||||||
"options": {
|
|
||||||
"cwd": "src/Yavsc.Api.Test",
|
|
||||||
"env": {
|
|
||||||
"YAVSC_API_TEST_DB_PROVIDER": "npgsql",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"group": {
|
|
||||||
"kind": "test",
|
|
||||||
"isDefault": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "build-webapi",
|
"label": "build-webapi",
|
||||||
"type": "process",
|
"type": "process",
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,6 @@ namespace PostIt.ViewModels.Commands;
|
||||||
|
|
||||||
public partial class RdvViewModel : BillingCommandPageViewModel
|
public partial class RdvViewModel : BillingCommandPageViewModel
|
||||||
{
|
{
|
||||||
private long? _existingLocationId;
|
|
||||||
private bool _hydratingExistingQuery;
|
|
||||||
|
|
||||||
public override string SupportMessage => "Complétez les informations du rendez-vous puis postez la commande.";
|
public override string SupportMessage => "Complétez les informations du rendez-vous puis postez la commande.";
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
|
@ -59,7 +56,6 @@ public partial class RdvViewModel : BillingCommandPageViewModel
|
||||||
|
|
||||||
protected override void ApplyExistingQuery(BillingQueryDetailsDto existingQuery)
|
protected override void ApplyExistingQuery(BillingQueryDetailsDto existingQuery)
|
||||||
{
|
{
|
||||||
_hydratingExistingQuery = true;
|
|
||||||
ExistingQueryId = existingQuery.Id;
|
ExistingQueryId = existingQuery.Id;
|
||||||
CommandStatus = existingQuery.Status;
|
CommandStatus = existingQuery.Status;
|
||||||
Consent = existingQuery.Consent;
|
Consent = existingQuery.Consent;
|
||||||
|
|
@ -74,18 +70,11 @@ public partial class RdvViewModel : BillingCommandPageViewModel
|
||||||
|
|
||||||
if (existingQuery.Location is not null)
|
if (existingQuery.Location is not null)
|
||||||
{
|
{
|
||||||
_existingLocationId = existingQuery.Location.Id;
|
|
||||||
Address = existingQuery.Location.Address ?? string.Empty;
|
Address = existingQuery.Location.Address ?? string.Empty;
|
||||||
SuggestedAddress = string.Empty;
|
SuggestedAddress = string.Empty;
|
||||||
Latitude = existingQuery.Location.Latitude;
|
Latitude = existingQuery.Location.Latitude;
|
||||||
Longitude = existingQuery.Location.Longitude;
|
Longitude = existingQuery.Location.Longitude;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
_existingLocationId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_hydratingExistingQuery = false;
|
|
||||||
|
|
||||||
this.SetInfoStatus($"Commande #{existingQuery.Id} chargée.");
|
this.SetInfoStatus($"Commande #{existingQuery.Id} chargée.");
|
||||||
}
|
}
|
||||||
|
|
@ -128,22 +117,20 @@ public partial class RdvViewModel : BillingCommandPageViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static BillingLocationDto BuildLocationPayload(string address, double? latitude, double? longitude, long? locationId = null)
|
protected static object BuildLocationPayload(string address, double? latitude, double? longitude)
|
||||||
{
|
{
|
||||||
if (latitude.HasValue && longitude.HasValue)
|
if (latitude.HasValue && longitude.HasValue)
|
||||||
{
|
{
|
||||||
return new BillingLocationDto
|
return new
|
||||||
{
|
{
|
||||||
Id = locationId,
|
|
||||||
Address = address,
|
Address = address,
|
||||||
Latitude = latitude.Value,
|
Latitude = latitude.Value,
|
||||||
Longitude = longitude.Value,
|
Longitude = longitude.Value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return new BillingLocationDto
|
return new
|
||||||
{
|
{
|
||||||
Id = locationId,
|
|
||||||
Address = address,
|
Address = address,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -236,30 +223,6 @@ public partial class RdvViewModel : BillingCommandPageViewModel
|
||||||
OnPropertyChanged(nameof(EventDateSelection));
|
OnPropertyChanged(nameof(EventDateSelection));
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnAddressChanged(string value)
|
|
||||||
{
|
|
||||||
if (_hydratingExistingQuery)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_existingLocationId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnLatitudeChanged(double? value)
|
|
||||||
{
|
|
||||||
if (_hydratingExistingQuery)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_existingLocationId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnLongitudeChanged(double? value)
|
|
||||||
{
|
|
||||||
if (_hydratingExistingQuery)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_existingLocationId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected override async Task SubmitAsync()
|
protected override async Task SubmitAsync()
|
||||||
{
|
{
|
||||||
|
|
@ -294,7 +257,7 @@ public partial class RdvViewModel : BillingCommandPageViewModel
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var address = Address.Trim();
|
var address = Address.Trim();
|
||||||
var locationPayload = BuildLocationPayload(address, Latitude, Longitude, IsEditingExisting ? _existingLocationId : null);
|
var locationPayload = BuildLocationPayload(address, Latitude, Longitude);
|
||||||
|
|
||||||
var payload = new BillingQueryDetailsDto
|
var payload = new BillingQueryDetailsDto
|
||||||
{
|
{
|
||||||
|
|
@ -307,7 +270,12 @@ public partial class RdvViewModel : BillingCommandPageViewModel
|
||||||
Status = CommandStatus,
|
Status = CommandStatus,
|
||||||
Reason = Reason.Trim(),
|
Reason = Reason.Trim(),
|
||||||
AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? string.Empty : AdditionalInfo.Trim(),
|
AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? string.Empty : AdditionalInfo.Trim(),
|
||||||
Location = locationPayload
|
Location = new BillingLocationDto
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
Latitude = Latitude,
|
||||||
|
Longitude = Longitude,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (IsEditingExisting)
|
if (IsEditingExisting)
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ public sealed class StatusNotice
|
||||||
|
|
||||||
(Glyph, Background, BorderBrush, Foreground) = severity switch
|
(Glyph, Background, BorderBrush, Foreground) = severity switch
|
||||||
{
|
{
|
||||||
StatusSeverity.Error => ("!", "#7F1D1D", "#C62828", "#e1f0f6"),
|
StatusSeverity.Error => ("!", "#FDECEA", "#C62828", "#7F1D1D"),
|
||||||
StatusSeverity.Warning => ("~", "#7C4A03", "#E6A700", "#eaeaea"),
|
StatusSeverity.Warning => ("~", "#FFF8E1", "#E6A700", "#7C4A03"),
|
||||||
_ => ("i", "#E8F0FE", "#5B8DEF", "#1E3A8A"),
|
_ => ("i", "#E8F0FE", "#5B8DEF", "#1E3A8A"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,6 @@ public sealed class BillingApiClient
|
||||||
? null
|
? null
|
||||||
: new BillingLocationDto
|
: new BillingLocationDto
|
||||||
{
|
{
|
||||||
Id = dto.Location.Id > 0 ? dto.Location.Id : null,
|
|
||||||
Address = dto.Location.Address ?? string.Empty,
|
Address = dto.Location.Address ?? string.Empty,
|
||||||
Latitude = dto.Location.Latitude,
|
Latitude = dto.Location.Latitude,
|
||||||
Longitude = dto.Location.Longitude,
|
Longitude = dto.Location.Longitude,
|
||||||
|
|
@ -192,7 +191,6 @@ public sealed class BillingApiClient
|
||||||
? null
|
? null
|
||||||
: new BillingLocationDto
|
: new BillingLocationDto
|
||||||
{
|
{
|
||||||
Id = dto.Location.Id > 0 ? dto.Location.Id : null,
|
|
||||||
Address = dto.Location.Address ?? string.Empty,
|
Address = dto.Location.Address ?? string.Empty,
|
||||||
Latitude = dto.Location.Latitude,
|
Latitude = dto.Location.Latitude,
|
||||||
Longitude = dto.Location.Longitude,
|
Longitude = dto.Location.Longitude,
|
||||||
|
|
@ -222,7 +220,6 @@ public sealed class BillingApiClient
|
||||||
? null
|
? null
|
||||||
: new BillingLocationDto
|
: new BillingLocationDto
|
||||||
{
|
{
|
||||||
Id = dto.Location.Id > 0 ? dto.Location.Id : null,
|
|
||||||
Address = dto.Location.Address ?? string.Empty,
|
Address = dto.Location.Address ?? string.Empty,
|
||||||
Latitude = dto.Location.Latitude,
|
Latitude = dto.Location.Latitude,
|
||||||
Longitude = dto.Location.Longitude,
|
Longitude = dto.Location.Longitude,
|
||||||
|
|
@ -316,11 +313,6 @@ public sealed class BillingApiClient
|
||||||
["Address"] = location.Address,
|
["Address"] = location.Address,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (location.Id.HasValue && location.Id.Value > 0)
|
|
||||||
{
|
|
||||||
payload["Id"] = location.Id.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (location.Latitude.HasValue)
|
if (location.Latitude.HasValue)
|
||||||
{
|
{
|
||||||
payload["Latitude"] = location.Latitude.Value;
|
payload["Latitude"] = location.Latitude.Value;
|
||||||
|
|
@ -336,7 +328,6 @@ public sealed class BillingApiClient
|
||||||
|
|
||||||
private sealed class BillingLocationResponse
|
private sealed class BillingLocationResponse
|
||||||
{
|
{
|
||||||
public long Id { get; set; }
|
|
||||||
public string? Address { get; set; }
|
public string? Address { get; set; }
|
||||||
public double Latitude { get; set; }
|
public double Latitude { get; set; }
|
||||||
public double Longitude { get; set; }
|
public double Longitude { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Yavsc.Blogspot;
|
using Yavsc.Blogspot;
|
||||||
|
|
||||||
namespace Yavsc.Api.Client;
|
namespace Yavsc.Api.Client;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using Yavsc;
|
using Yavsc;
|
||||||
|
|
||||||
namespace Yavsc.Api.Client;
|
namespace Yavsc.Api.Client;
|
||||||
|
|
@ -30,14 +29,7 @@ public sealed class BillingQueryDetailsDto
|
||||||
|
|
||||||
public sealed class BillingLocationDto
|
public sealed class BillingLocationDto
|
||||||
{
|
{
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
||||||
public long? Id { get; set; }
|
|
||||||
|
|
||||||
public string Address { get; set; } = string.Empty;
|
public string Address { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
||||||
public double? Latitude { get; set; }
|
public double? Latitude { get; set; }
|
||||||
|
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
||||||
public double? Longitude { get; set; }
|
public double? Longitude { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Yavsc.Api.Test.Fixtures;
|
using Yavsc.Api.Test.Fixtures;
|
||||||
using Yavsc.Helpers;
|
using Yavsc.Helpers;
|
||||||
using Yavsc.Models;
|
using Yavsc.Models;
|
||||||
using Yavsc.Models.Billing;
|
using Yavsc.Models.Billing;
|
||||||
using Yavsc.Models.Haircut;
|
|
||||||
using Yavsc.Models.Workflow;
|
using Yavsc.Models.Workflow;
|
||||||
using Yavsc.Tests.Shared;
|
using Yavsc.Tests.Shared;
|
||||||
|
|
||||||
|
|
@ -103,126 +101,6 @@ public sealed class BillingControllerTests : IClassFixture<ApiWebServerFixture>
|
||||||
Assert.Contains(payload!, item => item.BillingCode == BillingCodes.Rdv);
|
Assert.Contains(payload!, item => item.BillingCode == BillingCodes.Rdv);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetProviderOngoingCommands_ignores_rows_with_invalid_discriminator()
|
|
||||||
{
|
|
||||||
WorkflowHelpers.ConfigureBillingService();
|
|
||||||
_fixture.ResetAndSeedActivityGraph();
|
|
||||||
|
|
||||||
using (var scope = _fixture.Services.CreateScope())
|
|
||||||
{
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
||||||
|
|
||||||
db.Database.ExecuteSqlInterpolated($@"
|
|
||||||
INSERT INTO ""NominativeServiceCommand""
|
|
||||||
(""ActivityCode"", ""ClientId"", ""Consent"", ""DateCreated"", ""DateModified"", ""Description"", ""Discriminator"", ""PerformerId"", ""Status"", ""UserCreated"", ""UserModified"")
|
|
||||||
VALUES
|
|
||||||
({"dev"}, {"bob"}, {true}, {DateTime.UtcNow.AddMinutes(-5)}, {DateTime.UtcNow.AddMinutes(-4)}, {"Legacy malformed row"}, {""}, {"alice"}, {(int)QueryStatus.Accepted}, {"alice"}, {"alice"});
|
|
||||||
");
|
|
||||||
|
|
||||||
var location = db.Locations.Single();
|
|
||||||
db.RdvQueries.Add(new RdvQuery
|
|
||||||
{
|
|
||||||
ActivityCode = "dev",
|
|
||||||
ClientId = "bob",
|
|
||||||
PerformerId = "alice",
|
|
||||||
Consent = true,
|
|
||||||
UserCreated = "alice",
|
|
||||||
UserModified = "alice",
|
|
||||||
DateCreated = DateTime.UtcNow.AddMinutes(-3),
|
|
||||||
DateModified = DateTime.UtcNow.AddMinutes(-2),
|
|
||||||
EventDate = DateTime.UtcNow.AddDays(1),
|
|
||||||
Location = location,
|
|
||||||
Reason = "Commande valide",
|
|
||||||
Status = QueryStatus.InProgress,
|
|
||||||
Description = "Commande fournisseur valide",
|
|
||||||
});
|
|
||||||
|
|
||||||
db.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
using var http = NewClient();
|
|
||||||
|
|
||||||
var response = await http.GetAsync("/api/v1/bill/provider/ongoing", TestContext.Current.CancellationToken);
|
|
||||||
var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
|
||||||
|
|
||||||
Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}");
|
|
||||||
|
|
||||||
var payload = await response.Content.ReadFromJsonAsync<List<ProviderOngoingCommandDto>>(TestContext.Current.CancellationToken);
|
|
||||||
Assert.NotNull(payload);
|
|
||||||
Assert.NotEmpty(payload!);
|
|
||||||
Assert.Contains(payload!, item => item.BillingCode == BillingCodes.Rdv && item.PerformerId == "alice");
|
|
||||||
Assert.DoesNotContain(payload!, item => string.IsNullOrWhiteSpace(item.BillingCode));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetProviderOngoingCommands_returns_haircut_and_grouped_haircut_requests()
|
|
||||||
{
|
|
||||||
WorkflowHelpers.ConfigureBillingService();
|
|
||||||
_fixture.ResetAndSeedHaircutGraph();
|
|
||||||
|
|
||||||
using (var scope = _fixture.Services.CreateScope())
|
|
||||||
{
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
||||||
db.UserActivities.Add(new UserActivity
|
|
||||||
{
|
|
||||||
UserId = "alice",
|
|
||||||
DoesCode = "brush",
|
|
||||||
Weight = 50,
|
|
||||||
});
|
|
||||||
db.UserActivities.Add(new UserActivity
|
|
||||||
{
|
|
||||||
UserId = "alice",
|
|
||||||
DoesCode = "mbrush",
|
|
||||||
Weight = 50,
|
|
||||||
});
|
|
||||||
db.CommandForm.Add(new CommandForm
|
|
||||||
{
|
|
||||||
ActivityCode = "brush",
|
|
||||||
ActionName = BillingCodes.Brush,
|
|
||||||
Title = "Brush",
|
|
||||||
});
|
|
||||||
db.CommandForm.Add(new CommandForm
|
|
||||||
{
|
|
||||||
ActivityCode = "mbrush",
|
|
||||||
ActionName = BillingCodes.MBrush,
|
|
||||||
Title = "MBrush",
|
|
||||||
});
|
|
||||||
db.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
using var http = NewClient();
|
|
||||||
|
|
||||||
var response = await http.GetAsync("/api/v1/bill/provider/ongoing", TestContext.Current.CancellationToken);
|
|
||||||
var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
|
||||||
|
|
||||||
Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}");
|
|
||||||
|
|
||||||
var payload = await response.Content.ReadFromJsonAsync<List<ProviderOngoingCommandDto>>(TestContext.Current.CancellationToken);
|
|
||||||
Assert.NotNull(payload);
|
|
||||||
Assert.Contains(payload!, item => item.BillingCode == BillingCodes.Brush && item.PerformerId == "alice");
|
|
||||||
Assert.Contains(payload!, item => item.BillingCode == BillingCodes.MBrush && item.PerformerId == "alice");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetProviderOngoingCommands_excludes_requests_outside_performer_declared_activities()
|
|
||||||
{
|
|
||||||
WorkflowHelpers.ConfigureBillingService();
|
|
||||||
_fixture.ResetAndSeedHaircutGraph();
|
|
||||||
|
|
||||||
using var http = NewClient();
|
|
||||||
|
|
||||||
var response = await http.GetAsync("/api/v1/bill/provider/ongoing", TestContext.Current.CancellationToken);
|
|
||||||
var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
|
||||||
|
|
||||||
Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}");
|
|
||||||
|
|
||||||
var payload = await response.Content.ReadFromJsonAsync<List<ProviderOngoingCommandDto>>(TestContext.Current.CancellationToken);
|
|
||||||
Assert.NotNull(payload);
|
|
||||||
Assert.DoesNotContain(payload!, item => item.BillingCode == BillingCodes.Brush);
|
|
||||||
Assert.DoesNotContain(payload!, item => item.BillingCode == BillingCodes.MBrush);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ProviderOngoingCommandDto
|
private sealed class ProviderOngoingCommandDto
|
||||||
{
|
{
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Npgsql;
|
|
||||||
using Yavsc.Controllers;
|
using Yavsc.Controllers;
|
||||||
using Yavsc.Interfaces.Workflow;
|
using Yavsc.Interfaces.Workflow;
|
||||||
using Yavsc.Models;
|
using Yavsc.Models;
|
||||||
|
|
@ -20,27 +18,12 @@ namespace Yavsc.Api.Test.Fixtures;
|
||||||
|
|
||||||
public sealed class ApiWebServerFixture : WebHostFixture
|
public sealed class ApiWebServerFixture : WebHostFixture
|
||||||
{
|
{
|
||||||
private const string DbProviderEnvVar = "YAVSC_API_TEST_DB_PROVIDER";
|
|
||||||
private const string NpgsqlAdminConnectionEnvVar = "YAVSC_API_TEST_NPGSQL_ADMIN_CONNECTION";
|
|
||||||
private const string DedicatedNpgsqlDatabaseName = "yavscTestDb";
|
|
||||||
private const string DefaultDevelopmentConnectionString = "Server=localhost;Port=5432;Database=yavscdev;Username=yavscdev;Password=8*5idas;Include Error Detail=true";
|
|
||||||
|
|
||||||
protected override int HttpsPort => 5104;
|
protected override int HttpsPort => 5104;
|
||||||
|
|
||||||
private static SqliteConnection? _sharedSqliteConnection;
|
private static SqliteConnection? _sharedSqliteConnection;
|
||||||
private static readonly object _sqliteLock = new();
|
private static readonly object _sqliteLock = new();
|
||||||
private static readonly object _npgsqlLock = new();
|
|
||||||
private static string? _sharedNpgsqlConnectionString;
|
|
||||||
|
|
||||||
protected override WebApplication BuildApp(WebApplicationBuilder builder)
|
protected override WebApplication BuildApp(WebApplicationBuilder builder)
|
||||||
{
|
|
||||||
if (UseNpgsqlProvider())
|
|
||||||
{
|
|
||||||
var npgsqlConnectionString = EnsureNpgsqlDatabaseCreated();
|
|
||||||
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
|
|
||||||
opt.UseNpgsql(npgsqlConnectionString));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
SqliteConnection sharedConnection;
|
SqliteConnection sharedConnection;
|
||||||
lock (_sqliteLock)
|
lock (_sqliteLock)
|
||||||
|
|
@ -56,7 +39,6 @@ public sealed class ApiWebServerFixture : WebHostFixture
|
||||||
|
|
||||||
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
|
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
|
||||||
opt.UseSqlite(sharedConnection));
|
opt.UseSqlite(sharedConnection));
|
||||||
}
|
|
||||||
|
|
||||||
builder.Services.AddControllers()
|
builder.Services.AddControllers()
|
||||||
.AddApplicationPart(typeof(ActivityApiController).Assembly);
|
.AddApplicationPart(typeof(ActivityApiController).Assembly);
|
||||||
|
|
@ -108,76 +90,6 @@ public sealed class ApiWebServerFixture : WebHostFixture
|
||||||
|
|
||||||
public string BaseAddress => Addresses.First(a => a.StartsWith("https://", StringComparison.Ordinal));
|
public string BaseAddress => Addresses.First(a => a.StartsWith("https://", StringComparison.Ordinal));
|
||||||
|
|
||||||
public static bool UseNpgsqlProvider()
|
|
||||||
=> string.Equals(
|
|
||||||
Environment.GetEnvironmentVariable(DbProviderEnvVar),
|
|
||||||
"npgsql",
|
|
||||||
StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
private static string EnsureNpgsqlDatabaseCreated()
|
|
||||||
{
|
|
||||||
lock (_npgsqlLock)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(_sharedNpgsqlConnectionString))
|
|
||||||
{
|
|
||||||
return _sharedNpgsqlConnectionString;
|
|
||||||
}
|
|
||||||
|
|
||||||
var adminConnectionString = BuildAdminConnectionString();
|
|
||||||
var databaseName = DedicatedNpgsqlDatabaseName;
|
|
||||||
|
|
||||||
using (var adminConnection = new NpgsqlConnection(adminConnectionString))
|
|
||||||
{
|
|
||||||
adminConnection.Open();
|
|
||||||
using var existsCommand = adminConnection.CreateCommand();
|
|
||||||
existsCommand.CommandText = "SELECT 1 FROM pg_database WHERE datname = @databaseName";
|
|
||||||
existsCommand.Parameters.AddWithValue("databaseName", databaseName);
|
|
||||||
|
|
||||||
if (existsCommand.ExecuteScalar() is null)
|
|
||||||
{
|
|
||||||
using var createCommand = adminConnection.CreateCommand();
|
|
||||||
createCommand.CommandText = $"CREATE DATABASE \"{databaseName}\"";
|
|
||||||
createCommand.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var testConnectionBuilder = new NpgsqlConnectionStringBuilder(adminConnectionString)
|
|
||||||
{
|
|
||||||
Database = databaseName,
|
|
||||||
Pooling = false,
|
|
||||||
IncludeErrorDetail = true
|
|
||||||
};
|
|
||||||
|
|
||||||
_sharedNpgsqlConnectionString = testConnectionBuilder.ToString();
|
|
||||||
return _sharedNpgsqlConnectionString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildAdminConnectionString()
|
|
||||||
{
|
|
||||||
var configured = Environment.GetEnvironmentVariable(NpgsqlAdminConnectionEnvVar);
|
|
||||||
var source = string.IsNullOrWhiteSpace(configured)
|
|
||||||
? DefaultDevelopmentConnectionString
|
|
||||||
: configured;
|
|
||||||
|
|
||||||
var builder = new NpgsqlConnectionStringBuilder(source)
|
|
||||||
{
|
|
||||||
Pooling = false,
|
|
||||||
IncludeErrorDetail = true
|
|
||||||
};
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(configured))
|
|
||||||
{
|
|
||||||
builder.Database = "postgres";
|
|
||||||
}
|
|
||||||
else if (string.IsNullOrWhiteSpace(builder.Database))
|
|
||||||
{
|
|
||||||
builder.Database = "postgres";
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class NoopMessageSender : IYavscMessageSender
|
private sealed class NoopMessageSender : IYavscMessageSender
|
||||||
{
|
{
|
||||||
public Task<MessageWithPayloadResponse> NotifyBookQueryAsync(IEnumerable<string> connectionIds, RdvQueryEvent ev)
|
public Task<MessageWithPayloadResponse> NotifyBookQueryAsync(IEnumerable<string> connectionIds, RdvQueryEvent ev)
|
||||||
|
|
@ -198,7 +110,7 @@ public sealed class ApiWebServerFixture : WebHostFixture
|
||||||
using var scope = Services.CreateScope();
|
using var scope = Services.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||||
|
|
||||||
ResetDatabase(db);
|
db.Database.EnsureDeleted();
|
||||||
db.Database.EnsureCreated();
|
db.Database.EnsureCreated();
|
||||||
|
|
||||||
var user = new ApplicationUser
|
var user = new ApplicationUser
|
||||||
|
|
@ -299,94 +211,6 @@ public sealed class ApiWebServerFixture : WebHostFixture
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ResetDatabase(ApplicationDbContext db)
|
|
||||||
{
|
|
||||||
if (UseNpgsqlProvider())
|
|
||||||
{
|
|
||||||
db.Set<UserActivity>().RemoveRange(db.Set<UserActivity>());
|
|
||||||
db.Set<Activity>().RemoveRange(db.Set<Activity>());
|
|
||||||
db.Set<PerformerProfile>().RemoveRange(db.Set<PerformerProfile>());
|
|
||||||
db.Set<ApplicationUser>().RemoveRange(db.Set<ApplicationUser>());
|
|
||||||
db.Set<Location>().RemoveRange(db.Set<Location>());
|
|
||||||
db.Set<RdvQuery>().RemoveRange(db.Set<RdvQuery>());
|
|
||||||
db.Set<HairCutQuery>().RemoveRange(db.Set<HairCutQuery>());
|
|
||||||
db.Set<HairMultiCutQuery>().RemoveRange(db.Set<HairMultiCutQuery>());
|
|
||||||
db.Set<HairPrestation>().RemoveRange(db.Set<HairPrestation>());
|
|
||||||
db.Set<HairPrestationCollectionItem>().RemoveRange(db.Set<HairPrestationCollectionItem>());
|
|
||||||
|
|
||||||
db.SaveChanges();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
db.Database.EnsureDeleted();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private static IReadOnlyList<IEntityType> GetDeletionOrder(IModel model)
|
|
||||||
{
|
|
||||||
var entityTypes = model
|
|
||||||
.GetEntityTypes()
|
|
||||||
.Where(et =>
|
|
||||||
et.ClrType is not null &&
|
|
||||||
!et.IsOwned() &&
|
|
||||||
et.FindPrimaryKey() is not null)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var included = new HashSet<IEntityType>(entityTypes);
|
|
||||||
var dependencies = new Dictionary<IEntityType, HashSet<IEntityType>>();
|
|
||||||
|
|
||||||
foreach (var entityType in entityTypes)
|
|
||||||
{
|
|
||||||
var principals = entityType
|
|
||||||
.GetForeignKeys()
|
|
||||||
.Where(fk => !fk.IsOwnership)
|
|
||||||
.Select(fk => fk.PrincipalEntityType)
|
|
||||||
.Where(included.Contains)
|
|
||||||
.ToHashSet();
|
|
||||||
|
|
||||||
dependencies[entityType] = principals;
|
|
||||||
}
|
|
||||||
|
|
||||||
var queue = new Queue<IEntityType>(
|
|
||||||
dependencies.Where(kvp => kvp.Value.Count == 0).Select(kvp => kvp.Key));
|
|
||||||
|
|
||||||
var order = new List<IEntityType>(entityTypes.Length);
|
|
||||||
|
|
||||||
while (queue.Count > 0)
|
|
||||||
{
|
|
||||||
var current = queue.Dequeue();
|
|
||||||
if (!order.Contains(current))
|
|
||||||
{
|
|
||||||
order.Add(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var kvp in dependencies)
|
|
||||||
{
|
|
||||||
if (!kvp.Value.Remove(current) || kvp.Value.Count != 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!order.Contains(kvp.Key) && !queue.Contains(kvp.Key))
|
|
||||||
{
|
|
||||||
queue.Enqueue(kvp.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If cycles remain (rare), append unresolved types last and rely on DB cascades.
|
|
||||||
foreach (var entityType in entityTypes)
|
|
||||||
{
|
|
||||||
if (!order.Contains(entityType))
|
|
||||||
{
|
|
||||||
order.Add(entityType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return order;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResetAndSeedRdvQueryGraph()
|
public void ResetAndSeedRdvQueryGraph()
|
||||||
{
|
{
|
||||||
ResetAndSeedActivityGraph();
|
ResetAndSeedActivityGraph();
|
||||||
|
|
|
||||||
|
|
@ -143,87 +143,4 @@ public sealed class RdvQueryApiControllerTests : IClassFixture<ApiWebServerFixtu
|
||||||
Assert.NotNull(created);
|
Assert.NotNull(created);
|
||||||
Assert.Equal(DateTimeKind.Utc, created!.EventDate.Kind);
|
Assert.Equal(DateTimeKind.Utc, created!.EventDate.Kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PostQuery_with_unknown_location_id_creates_location_and_succeeds()
|
|
||||||
{
|
|
||||||
_fixture.ResetAndSeedRdvQueryGraph();
|
|
||||||
using var http = NewClient(subject: "alice");
|
|
||||||
|
|
||||||
var createPayload = new
|
|
||||||
{
|
|
||||||
ActivityCode = "dev",
|
|
||||||
PerformerId = "alice",
|
|
||||||
Consent = true,
|
|
||||||
EventDate = DateTime.UtcNow.AddDays(3),
|
|
||||||
Location = new
|
|
||||||
{
|
|
||||||
Id = 999999L,
|
|
||||||
Address = "4 rue du Test",
|
|
||||||
Latitude = 48.8569,
|
|
||||||
Longitude = 2.3525,
|
|
||||||
},
|
|
||||||
Reason = "Rendez-vous id location inconnu",
|
|
||||||
Status = QueryStatus.Inserted,
|
|
||||||
};
|
|
||||||
|
|
||||||
var createResponse = await http.PostAsJsonAsync("/api/v1/billing/Rdv", createPayload, TestContext.Current.CancellationToken);
|
|
||||||
var body = await createResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
|
||||||
|
|
||||||
Assert.True(createResponse.StatusCode == HttpStatusCode.Created, $"Unexpected status {(int)createResponse.StatusCode} ({createResponse.StatusCode}): {body}");
|
|
||||||
|
|
||||||
var created = await createResponse.Content.ReadFromJsonAsync<RdvQuery>(TestContext.Current.CancellationToken);
|
|
||||||
Assert.NotNull(created);
|
|
||||||
Assert.NotNull(created!.Location);
|
|
||||||
Assert.True(created.Location.Id > 0);
|
|
||||||
Assert.NotEqual(999999L, created.Location.Id);
|
|
||||||
Assert.Equal("alice", created.ClientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PostQuery_without_location_returns_bad_request()
|
|
||||||
{
|
|
||||||
_fixture.ResetAndSeedRdvQueryGraph();
|
|
||||||
using var http = NewClient(subject: "alice");
|
|
||||||
|
|
||||||
var createPayload = new
|
|
||||||
{
|
|
||||||
ActivityCode = "dev",
|
|
||||||
PerformerId = "alice",
|
|
||||||
Consent = true,
|
|
||||||
EventDate = DateTime.UtcNow.AddDays(1),
|
|
||||||
Reason = "Rendez-vous sans location",
|
|
||||||
Status = QueryStatus.Inserted,
|
|
||||||
};
|
|
||||||
|
|
||||||
var createResponse = await http.PostAsJsonAsync("/api/v1/billing/Rdv", createPayload, TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, createResponse.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PostQuery_with_unknown_location_id_and_missing_address_returns_bad_request()
|
|
||||||
{
|
|
||||||
_fixture.ResetAndSeedRdvQueryGraph();
|
|
||||||
using var http = NewClient(subject: "alice");
|
|
||||||
|
|
||||||
var createPayload = new
|
|
||||||
{
|
|
||||||
ActivityCode = "dev",
|
|
||||||
PerformerId = "alice",
|
|
||||||
Consent = true,
|
|
||||||
EventDate = DateTime.UtcNow.AddDays(1),
|
|
||||||
Location = new
|
|
||||||
{
|
|
||||||
Id = 777777L,
|
|
||||||
Address = "",
|
|
||||||
Latitude = 0.0,
|
|
||||||
Longitude = 0.0,
|
|
||||||
},
|
|
||||||
Reason = "Rendez-vous location invalide",
|
|
||||||
Status = QueryStatus.Inserted,
|
|
||||||
};
|
|
||||||
|
|
||||||
var createResponse = await http.PostAsJsonAsync("/api/v1/billing/Rdv", createPayload, TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, createResponse.StatusCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ using Yavsc.Billing;
|
||||||
using Yavsc.Helpers;
|
using Yavsc.Helpers;
|
||||||
using Yavsc.ViewModels;
|
using Yavsc.ViewModels;
|
||||||
using Yavsc.Models.Billing;
|
using Yavsc.Models.Billing;
|
||||||
using Yavsc.Models.Haircut;
|
|
||||||
using Yavsc.Models.Workflow;
|
|
||||||
using Yavsc.Server.Models.FileSystem;
|
using Yavsc.Server.Models.FileSystem;
|
||||||
|
|
||||||
namespace Yavsc.ApiControllers
|
namespace Yavsc.ApiControllers
|
||||||
|
|
@ -123,69 +121,12 @@ namespace Yavsc.ApiControllers
|
||||||
WorkflowHelpers.ConfigureBillingService();
|
WorkflowHelpers.ConfigureBillingService();
|
||||||
}
|
}
|
||||||
|
|
||||||
var allowedActivityCodes = dbContext.UserActivities
|
var commands = dbContext.Set<NominativeServiceCommand>()
|
||||||
.AsNoTracking()
|
|
||||||
.Where(a => a.UserId == uid)
|
|
||||||
.Select(a => a.DoesCode)
|
|
||||||
.Distinct()
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (allowedActivityCodes.Count == 0)
|
|
||||||
{
|
|
||||||
return Ok(Array.Empty<object>());
|
|
||||||
}
|
|
||||||
|
|
||||||
var allowedBillingCodes = dbContext.CommandForm
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(form => allowedActivityCodes.Contains(form.ActivityCode))
|
|
||||||
.Select(form => form.ActionName)
|
|
||||||
.Where(actionName => !string.IsNullOrWhiteSpace(actionName))
|
|
||||||
.Distinct()
|
|
||||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
var fallbackToActivityFilteringOnly = allowedBillingCodes.Count == 0;
|
|
||||||
|
|
||||||
// Query only the command types allowed by the performer's declared
|
|
||||||
// activities; this avoids touching unrelated legacy slices.
|
|
||||||
var rdvCommands = fallbackToActivityFilteringOnly || allowedBillingCodes.Contains(BillingCodes.Rdv)
|
|
||||||
? dbContext.Set<RdvQuery>()
|
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(q => q.PerformerId == uid)
|
.Where(q => q.PerformerId == uid)
|
||||||
.Where(q => allowedActivityCodes.Contains(q.ActivityCode))
|
|
||||||
.Where(q => q.Status == QueryStatus.Inserted
|
.Where(q => q.Status == QueryStatus.Inserted
|
||||||
|| q.Status == QueryStatus.Accepted
|
|| q.Status == QueryStatus.Accepted
|
||||||
|| q.Status == QueryStatus.InProgress)
|
|| q.Status == QueryStatus.InProgress)
|
||||||
.Cast<NominativeServiceCommand>()
|
|
||||||
.ToList()
|
|
||||||
: new List<NominativeServiceCommand>();
|
|
||||||
|
|
||||||
var hairCommands = fallbackToActivityFilteringOnly || allowedBillingCodes.Contains(BillingCodes.Brush)
|
|
||||||
? dbContext.Set<HairCutQuery>()
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(q => q.PerformerId == uid)
|
|
||||||
.Where(q => allowedActivityCodes.Contains(q.ActivityCode))
|
|
||||||
.Where(q => q.Status == QueryStatus.Inserted
|
|
||||||
|| q.Status == QueryStatus.Accepted
|
|
||||||
|| q.Status == QueryStatus.InProgress)
|
|
||||||
.Cast<NominativeServiceCommand>()
|
|
||||||
.ToList()
|
|
||||||
: new List<NominativeServiceCommand>();
|
|
||||||
|
|
||||||
var hairMultiCommands = fallbackToActivityFilteringOnly || allowedBillingCodes.Contains(BillingCodes.MBrush)
|
|
||||||
? dbContext.Set<HairMultiCutQuery>()
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(q => q.PerformerId == uid)
|
|
||||||
.Where(q => allowedActivityCodes.Contains(q.ActivityCode))
|
|
||||||
.Where(q => q.Status == QueryStatus.Inserted
|
|
||||||
|| q.Status == QueryStatus.Accepted
|
|
||||||
|| q.Status == QueryStatus.InProgress)
|
|
||||||
.Cast<NominativeServiceCommand>()
|
|
||||||
.ToList()
|
|
||||||
: new List<NominativeServiceCommand>();
|
|
||||||
|
|
||||||
var commands = rdvCommands
|
|
||||||
.Concat(hairCommands)
|
|
||||||
.Concat(hairMultiCommands)
|
|
||||||
.OrderByDescending(q => q.DateModified)
|
.OrderByDescending(q => q.DateModified)
|
||||||
.ThenByDescending(q => q.Id)
|
.ThenByDescending(q => q.Id)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
|
||||||
193
src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs
Normal file
193
src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Yavsc.Controllers
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using Yavsc.Models;
|
||||||
|
using Yavsc.Models.Workflow;
|
||||||
|
using Yavsc.Models.Billing;
|
||||||
|
using Yavsc.Abstract.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Yavsc.Server.Helpers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[Produces("application/json")]
|
||||||
|
[Route(Constants.APIPrefix + "/bookquery"), Authorize("Performer")]
|
||||||
|
public class BookQueryApiController : Controller
|
||||||
|
{
|
||||||
|
private ApplicationDbContext _context;
|
||||||
|
private ILogger _logger;
|
||||||
|
|
||||||
|
public BookQueryApiController(ApplicationDbContext context, ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = loggerFactory.CreateLogger<BookQueryApiController>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: api/BookQueryApi
|
||||||
|
/// <summary>
|
||||||
|
/// Book queries, by creation order
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxId">returned Ids must be lower than this value</param>
|
||||||
|
/// <returns>book queries</returns>
|
||||||
|
[HttpGet]
|
||||||
|
public IEnumerable<RdvQueryProviderInfo> GetCommands(long maxId=long.MaxValue)
|
||||||
|
{
|
||||||
|
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
var result = _context.RdvQueries.Include(c => c.Location).
|
||||||
|
Include(c => c.Client).Where(c => c.PerformerId == uid && c.Id < maxId && c.EventDate > now
|
||||||
|
&& c.ValidationDate == null).
|
||||||
|
Select(c => new RdvQueryProviderInfo
|
||||||
|
{
|
||||||
|
Client = new ClientProviderInfo {
|
||||||
|
UserName = c.Client.UserName,
|
||||||
|
UserId = c.ClientId,
|
||||||
|
Avatar = c.Client.Avatar },
|
||||||
|
Location = c.Location,
|
||||||
|
EventDate = c.EventDate,
|
||||||
|
Id = c.Id,
|
||||||
|
Previsional = c.Provisional,
|
||||||
|
Reason = c.Reason,
|
||||||
|
ActivityCode = c.ActivityCode,
|
||||||
|
BillingCode = BillingCodes.Rdv
|
||||||
|
}).
|
||||||
|
OrderBy(c=>c.Id).
|
||||||
|
Take(25);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: api/BookQueryApi/5
|
||||||
|
[HttpGet("{id}", Name = "GetBookQuery")]
|
||||||
|
public IActionResult GetBookQuery([FromRoute] long id)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
|
||||||
|
RdvQuery bookQuery = _context.RdvQueries.Where(c => c.ClientId == uid || c.PerformerId == uid).Single(m => m.Id == id);
|
||||||
|
|
||||||
|
if (bookQuery == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(bookQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT: api/BookQueryApi/5
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public IActionResult PutBookQuery(long id, [FromBody] RdvQuery bookQuery)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id != bookQuery.Id)
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
if (bookQuery.ClientId != uid)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
_context.Entry(bookQuery).State = EntityState.Modified;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_context.SaveChanges(User.GetUserId());
|
||||||
|
}
|
||||||
|
catch (DbUpdateConcurrencyException)
|
||||||
|
{
|
||||||
|
if (!BookQueryExists(id))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StatusCodeResult(StatusCodes.Status204NoContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/BookQueryApi
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult PostBookQuery([FromBody] RdvQuery bookQuery)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
if (bookQuery.ClientId != uid)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError("ClientId", "You must be the client at creating a book query");
|
||||||
|
return new BadRequestObjectResult(ModelState);
|
||||||
|
}
|
||||||
|
_context.RdvQueries.Add(bookQuery);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_context.SaveChanges(User.GetUserId());
|
||||||
|
}
|
||||||
|
catch (DbUpdateException)
|
||||||
|
{
|
||||||
|
if (BookQueryExists(bookQuery.Id))
|
||||||
|
{
|
||||||
|
return new StatusCodeResult(StatusCodes.Status409Conflict);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreatedAtRoute("GetBookQuery", new { id = bookQuery.Id }, bookQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE: api/BookQueryApi/5
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public IActionResult DeleteBookQuery(long id)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
RdvQuery bookQuery = _context.RdvQueries.Single(m => m.Id == id);
|
||||||
|
|
||||||
|
if (bookQuery == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
if (bookQuery.ClientId != uid) return NotFound();
|
||||||
|
|
||||||
|
_context.RdvQueries.Remove(bookQuery);
|
||||||
|
_context.SaveChanges(User.GetUserId());
|
||||||
|
|
||||||
|
return Ok(bookQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_context.Dispose();
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool BookQueryExists(long id)
|
||||||
|
{
|
||||||
|
return _context.RdvQueries.Count(e => e.Id == id) > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
#nullable enable annotations
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
|
||||||
using Npgsql;
|
|
||||||
using Yavsc.Models;
|
using Yavsc.Models;
|
||||||
using Yavsc.Models.Billing;
|
using Yavsc.Models.Billing;
|
||||||
using Yavsc.Models.Relationship;
|
|
||||||
using Yavsc.Models.Workflow;
|
using Yavsc.Models.Workflow;
|
||||||
using Yavsc.Server.Helpers;
|
using Yavsc.Server.Helpers;
|
||||||
|
|
||||||
|
|
@ -88,32 +83,30 @@ public class RdvQueryApiController : Controller
|
||||||
return BadRequest(ModelState);
|
return BadRequest(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.Location is null)
|
if (query.Location is not null)
|
||||||
{
|
{
|
||||||
return BadRequest(new { Error = "location is required" });
|
var existingLocation = await _context.Locations.FirstOrDefaultAsync(
|
||||||
|
x => x.Address == query.Location.Address
|
||||||
|
&& x.Longitude == query.Location.Longitude
|
||||||
|
&& x.Latitude == query.Location.Latitude,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (existingLocation is not null)
|
||||||
|
{
|
||||||
|
query.Location = existingLocation;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_context.Attach(query.Location);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var resolvedLocation = await ResolveLocationAsync(query.Location, cancellationToken);
|
_context.RdvQueries.Add(query);
|
||||||
if (resolvedLocation is null)
|
|
||||||
{
|
|
||||||
return BadRequest(new { Error = "location payload is invalid" });
|
|
||||||
}
|
|
||||||
|
|
||||||
await PersistLocationIfNeededAsync(resolvedLocation, uid, cancellationToken);
|
|
||||||
|
|
||||||
query.Location = resolvedLocation;
|
|
||||||
|
|
||||||
var addedEntry = _context.RdvQueries.Add(query);
|
|
||||||
EnsureLocationForeignKey(addedEntry, resolvedLocation.Id);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _context.SaveChangesAsync(User.GetUserId(), cancellationToken);
|
await _context.SaveChangesAsync(User.GetUserId(), cancellationToken);
|
||||||
}
|
}
|
||||||
catch (DbUpdateException ex) when (IsLocationForeignKeyViolation(ex))
|
|
||||||
{
|
|
||||||
return BadRequest(new { Error = "location reference is invalid" });
|
|
||||||
}
|
|
||||||
catch (DbUpdateException)
|
catch (DbUpdateException)
|
||||||
{
|
{
|
||||||
if (QueryExists(query.Id))
|
if (QueryExists(query.Id))
|
||||||
|
|
@ -156,16 +149,17 @@ public class RdvQueryApiController : Controller
|
||||||
|
|
||||||
if (query.Location is not null)
|
if (query.Location is not null)
|
||||||
{
|
{
|
||||||
var resolvedLocation = await ResolveLocationAsync(query.Location, cancellationToken);
|
var resolvedLocation = await _context.Locations.FirstOrDefaultAsync(
|
||||||
|
x => x.Address == query.Location.Address
|
||||||
|
&& x.Longitude == query.Location.Longitude
|
||||||
|
&& x.Latitude == query.Location.Latitude,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
existing.Location = resolvedLocation ?? query.Location;
|
||||||
if (resolvedLocation is null)
|
if (resolvedLocation is null)
|
||||||
{
|
{
|
||||||
return BadRequest(new { Error = "location payload is invalid" });
|
_context.Attach(query.Location);
|
||||||
}
|
}
|
||||||
|
|
||||||
await PersistLocationIfNeededAsync(resolvedLocation, uid, cancellationToken);
|
|
||||||
|
|
||||||
existing.Location = resolvedLocation;
|
|
||||||
EnsureLocationForeignKey(_context.Entry(existing), resolvedLocation.Id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -181,10 +175,6 @@ public class RdvQueryApiController : Controller
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (DbUpdateException ex) when (IsLocationForeignKeyViolation(ex))
|
|
||||||
{
|
|
||||||
return BadRequest(new { Error = "location reference is invalid" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
@ -218,78 +208,6 @@ public class RdvQueryApiController : Controller
|
||||||
return _context.RdvQueries.Any(e => e.Id == id);
|
return _context.RdvQueries.Any(e => e.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Location?> ResolveLocationAsync(Location postedLocation, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (postedLocation.Id > 0)
|
|
||||||
{
|
|
||||||
var byId = await _context.Locations
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == postedLocation.Id, cancellationToken);
|
|
||||||
if (byId is not null)
|
|
||||||
{
|
|
||||||
return byId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(postedLocation.Address))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var existingByCoordinates = await _context.Locations.FirstOrDefaultAsync(
|
|
||||||
x => x.Address == postedLocation.Address
|
|
||||||
&& x.Longitude == postedLocation.Longitude
|
|
||||||
&& x.Latitude == postedLocation.Latitude,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
if (existingByCoordinates is not null)
|
|
||||||
{
|
|
||||||
return existingByCoordinates;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Treat unknown location ids as client-side placeholders and insert a new row.
|
|
||||||
postedLocation.Id = 0;
|
|
||||||
_context.Locations.Add(postedLocation);
|
|
||||||
return postedLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PersistLocationIfNeededAsync(Location location, string userId, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (_context.Entry(location).State != EntityState.Added)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync(userId, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsLocationForeignKeyViolation(DbUpdateException ex)
|
|
||||||
{
|
|
||||||
if (ex.InnerException is not PostgresException pg)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pg.SqlState == PostgresErrorCodes.ForeignKeyViolation
|
|
||||||
&& string.Equals(pg.ConstraintName, "FK_NominativeServiceCommand_Locations_LocationId", StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsureLocationForeignKey(EntityEntry<RdvQuery> entry, long locationId)
|
|
||||||
{
|
|
||||||
SetFkIfPresent(entry, "LocationId", locationId);
|
|
||||||
SetFkIfPresent(entry, "RdvQuery_LocationId", locationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SetFkIfPresent(EntityEntry<RdvQuery> entry, string propertyName, long value)
|
|
||||||
{
|
|
||||||
var property = entry.Metadata.FindProperty(propertyName);
|
|
||||||
if (property is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.Property(propertyName).CurrentValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DateTime EnsureUtc(DateTime value)
|
private static DateTime EnsureUtc(DateTime value)
|
||||||
{
|
{
|
||||||
return value.Kind switch
|
return value.Kind switch
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,6 @@ internal class Program
|
||||||
services.AddAuthentication("Bearer")
|
services.AddAuthentication("Bearer")
|
||||||
.AddYavscJwtBearer(builder.Configuration);
|
.AddYavscJwtBearer(builder.Configuration);
|
||||||
|
|
||||||
services.AddSignalR();
|
|
||||||
services.AddSingleton<IConnexionManager, HubConnectionManager>();
|
|
||||||
|
|
||||||
// DbContextBuilder
|
// DbContextBuilder
|
||||||
services.AddDbContext<ApplicationDbContext>(options =>
|
services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
options.UseNpgsql(builder.Configuration.GetConnectionString(
|
options.UseNpgsql(builder.Configuration.GetConnectionString(
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Yavsc.Models;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Yavsc.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
|
||||||
[Migration("20260913022000_cleanupLegacyNominativeServiceCommandLocationId")]
|
|
||||||
public partial class cleanupLegacyNominativeServiceCommandLocationId : Migration
|
|
||||||
{
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.Sql(@"
|
|
||||||
UPDATE ""NominativeServiceCommand""
|
|
||||||
SET ""RdvQuery_LocationId"" = COALESCE(""RdvQuery_LocationId"", ""LocationId"")
|
|
||||||
WHERE ""Discriminator"" = 'RdvQuery'
|
|
||||||
AND ""LocationId"" IS NOT NULL;
|
|
||||||
");
|
|
||||||
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_NominativeServiceCommand_Locations_LocationId",
|
|
||||||
table: "NominativeServiceCommand");
|
|
||||||
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "IX_NominativeServiceCommand_LocationId",
|
|
||||||
table: "NominativeServiceCommand");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "LocationId",
|
|
||||||
table: "NominativeServiceCommand");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<long>(
|
|
||||||
name: "LocationId",
|
|
||||||
table: "NominativeServiceCommand",
|
|
||||||
type: "bigint",
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.Sql(@"
|
|
||||||
UPDATE ""NominativeServiceCommand""
|
|
||||||
SET ""LocationId"" = ""RdvQuery_LocationId""
|
|
||||||
WHERE ""Discriminator"" = 'RdvQuery'
|
|
||||||
AND ""RdvQuery_LocationId"" IS NOT NULL;
|
|
||||||
");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_NominativeServiceCommand_LocationId",
|
|
||||||
table: "NominativeServiceCommand",
|
|
||||||
column: "LocationId");
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_NominativeServiceCommand_Locations_LocationId",
|
|
||||||
table: "NominativeServiceCommand",
|
|
||||||
column: "LocationId",
|
|
||||||
principalTable: "Locations",
|
|
||||||
principalColumn: "Id");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Yavsc.Abstract.Chat;
|
using Yavsc.Abstract.Chat;
|
||||||
using Yavsc.Models;
|
using Yavsc.Models;
|
||||||
using Yavsc.ViewModels.Chat;
|
using Yavsc.ViewModels.Chat;
|
||||||
|
|
@ -121,10 +119,10 @@ namespace Yavsc.Services
|
||||||
|
|
||||||
public bool Part(string cxId, string roomName, string reason)
|
public bool Part(string cxId, string roomName, string reason)
|
||||||
{
|
{
|
||||||
ChatRoomInfo channelInfo;
|
ChatRoomInfo chanInfo;
|
||||||
if (Channels.TryGetValue(roomName, out channelInfo))
|
if (Channels.TryGetValue(roomName, out chanInfo))
|
||||||
{
|
{
|
||||||
if (!channelInfo.Users.Contains(cxId))
|
if (!chanInfo.Users.Contains(cxId))
|
||||||
{
|
{
|
||||||
// TODO NotifyErrorToCaller(roomName, "you didn't join.");
|
// TODO NotifyErrorToCaller(roomName, "you didn't join.");
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -132,11 +130,11 @@ namespace Yavsc.Services
|
||||||
// FIXME only remove cx, not username,
|
// FIXME only remove cx, not username,
|
||||||
// as long as he might be connected
|
// as long as he might be connected
|
||||||
// from another device, to the same room
|
// from another device, to the same room
|
||||||
channelInfo.Users.Remove(cxId);
|
chanInfo.Users.Remove(cxId);
|
||||||
if (channelInfo.Users.Count == 0)
|
if (chanInfo.Users.Count == 0)
|
||||||
{
|
{
|
||||||
ChatRoomInfo deadChannelInfo;
|
ChatRoomInfo deadchanInfo;
|
||||||
if (Channels.TryRemove(roomName, out deadChannelInfo))
|
if (Channels.TryRemove(roomName, out deadchanInfo))
|
||||||
{
|
{
|
||||||
var room = _dbContext.ChatRoom.FirstOrDefault(r => r.Name == roomName);
|
var room = _dbContext.ChatRoom.FirstOrDefault(r => r.Name == roomName);
|
||||||
room.LatestJoinPart = DateTime.UtcNow;
|
room.LatestJoinPart = DateTime.UtcNow;
|
||||||
|
|
@ -157,67 +155,67 @@ namespace Yavsc.Services
|
||||||
var userName = ChatUserNames[cxId];
|
var userName = ChatUserNames[cxId];
|
||||||
|
|
||||||
_logger.LogInformation($"Join: {userName}=>{roomName}");
|
_logger.LogInformation($"Join: {userName}=>{roomName}");
|
||||||
ChatRoomInfo channelInfo;
|
ChatRoomInfo chanInfo;
|
||||||
// if channel already is open
|
// if channel already is open
|
||||||
if (Channels.ContainsKey(roomName))
|
if (Channels.ContainsKey(roomName))
|
||||||
{
|
{
|
||||||
if (Channels.TryGetValue(roomName, out channelInfo))
|
if (Channels.TryGetValue(roomName, out chanInfo))
|
||||||
{
|
{
|
||||||
if (IsPresent(roomName, userName))
|
if (IsPresent(roomName, userName))
|
||||||
{
|
{
|
||||||
// TODO implement some unique connection sharing protocol
|
// TODO implement some unique connection sharing protocol
|
||||||
// between all terminals from a single user.
|
// between all terminals from a single user.
|
||||||
return channelInfo;
|
return chanInfo;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (IsCop(userName))
|
if (IsCop(userName))
|
||||||
{
|
{
|
||||||
channelInfo.Ops.Add(cxId);
|
chanInfo.Ops.Add(cxId);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
channelInfo.Users.Add(cxId);
|
chanInfo.Users.Add(cxId);
|
||||||
}
|
}
|
||||||
_logger.LogInformation($"existing room joint: {userName}=>{roomName}");
|
_logger.LogInformation($"existing room joint: {userName}=>{roomName}");
|
||||||
if (!ChatRoomPresence[userName].Contains(roomName))
|
if (!ChatRoomPresence[userName].Contains(roomName))
|
||||||
ChatRoomPresence[userName].Add(roomName);
|
ChatRoomPresence[userName].Add(roomName);
|
||||||
return channelInfo;
|
return chanInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string msg = "room seemed to be available ... but we could get no info on it.";
|
string msg = "room seemd to be avaible ... but we could get no info on it.";
|
||||||
_errorHandler(roomName, msg);
|
_errorHandler(roomName, msg);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// room was closed.
|
// room was closed.
|
||||||
var room = _dbContext.ChatRoom.FirstOrDefault(r => r.Name == roomName);
|
var room = _dbContext.ChatRoom.FirstOrDefault(r => r.Name == roomName);
|
||||||
channelInfo = new ChatRoomInfo();
|
chanInfo = new ChatRoomInfo();
|
||||||
|
|
||||||
|
|
||||||
if (room != null)
|
if (room != null)
|
||||||
{
|
{
|
||||||
channelInfo.Topic = room.Topic;
|
chanInfo.Topic = room.Topic;
|
||||||
channelInfo.Name = room.Name;
|
chanInfo.Name = room.Name;
|
||||||
channelInfo.Users.Add(cxId);
|
chanInfo.Users.Add(cxId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{ // a first join, we create it.
|
{ // a first join, we create it.
|
||||||
channelInfo.Name = roomName;
|
chanInfo.Name = roomName;
|
||||||
channelInfo.Topic = _localizer.GetString(ChatHubConstants.JustCreatedBy)+userName;
|
chanInfo.Topic = _localizer.GetString(ChatHubConstants.JustCreatedBy)+userName;
|
||||||
channelInfo.Ops.Add(cxId);
|
chanInfo.Ops.Add(cxId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Channels.TryAdd(roomName, channelInfo))
|
if (Channels.TryAdd(roomName, chanInfo))
|
||||||
{
|
{
|
||||||
ChatRoomPresence[userName].Add(roomName);
|
ChatRoomPresence[userName].Add(roomName);
|
||||||
_logger.LogInformation("new room joint");
|
_logger.LogInformation("new room joint");
|
||||||
return (channelInfo);
|
return (chanInfo);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string msg = "Chan create failed unexpectedly...";
|
string msg = "Chan create failed unexpectly...";
|
||||||
_errorHandler(roomName, msg);
|
_errorHandler(roomName, msg);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -228,7 +226,7 @@ namespace Yavsc.Services
|
||||||
throw new System.NotImplementedException();
|
throw new System.NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool DeOp(string roomName, string userName)
|
public bool Deop(string roomName, string userName)
|
||||||
{
|
{
|
||||||
throw new System.NotImplementedException();
|
throw new System.NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
@ -248,9 +246,9 @@ namespace Yavsc.Services
|
||||||
return ChatUserNames[cxId];
|
return ChatUserNames[cxId];
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetChanInfo(string room, out ChatRoomInfo channelInfo)
|
public bool TryGetChanInfo(string room, out ChatRoomInfo chanInfo)
|
||||||
{
|
{
|
||||||
return Channels.TryGetValue(room, out channelInfo);
|
return Channels.TryGetValue(room, out chanInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<ChannelShortInfo> ListChannels(string pattern)
|
public IEnumerable<ChannelShortInfo> ListChannels(string pattern)
|
||||||
|
|
@ -279,22 +277,22 @@ namespace Yavsc.Services
|
||||||
|
|
||||||
public bool Kick(string cxId, string userName, string roomName, string reason)
|
public bool Kick(string cxId, string userName, string roomName, string reason)
|
||||||
{
|
{
|
||||||
ChatRoomInfo channelInfo;
|
ChatRoomInfo chanInfo;
|
||||||
if (!Channels.ContainsKey(roomName))
|
if (!Channels.ContainsKey(roomName))
|
||||||
{
|
{
|
||||||
_errorHandler(roomName, _localizer.GetString(ChatHubConstants.LabNoSuchChan).ToString());
|
_errorHandler(roomName, _localizer.GetString(ChatHubConstants.LabNoSuchChan).ToString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Channels.TryGetValue(roomName, out channelInfo))
|
if (!Channels.TryGetValue(roomName, out chanInfo))
|
||||||
{
|
{
|
||||||
_errorHandler(roomName, _localizer.GetString(ChatHubConstants.LabNoSuchChan).ToString());
|
_errorHandler(roomName, _localizer.GetString(ChatHubConstants.LabNoSuchChan).ToString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var kickerName = GetUserName(cxId);
|
var kickerName = GetUserName(cxId);
|
||||||
if (!channelInfo.Ops.Contains(cxId))
|
if (!chanInfo.Ops.Contains(cxId))
|
||||||
if (!channelInfo.Hops.Contains(cxId))
|
if (!chanInfo.Hops.Contains(cxId))
|
||||||
{
|
{
|
||||||
_errorHandler(roomName, _localizer.GetString(ChatHubConstants.LabYouNotOp).ToString());
|
_errorHandler(roomName, _localizer.GetString(ChatHubConstants.LabYouNotOp).ToString());
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -305,9 +303,9 @@ namespace Yavsc.Services
|
||||||
_errorHandler(roomName, _localizer.GetString(ChatHubConstants.LabNoSuchUser).ToString());
|
_errorHandler(roomName, _localizer.GetString(ChatHubConstants.LabNoSuchUser).ToString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var userConnectionIds = GetConnexionIds(userName);
|
var ucxs = GetConnexionIds(userName);
|
||||||
if (channelInfo.Hops.Contains(cxId))
|
if (chanInfo.Hops.Contains(cxId))
|
||||||
if (channelInfo.Ops.Any(c => userConnectionIds.Contains(c)))
|
if (chanInfo.Ops.Any(c => ucxs.Contains(c)))
|
||||||
{
|
{
|
||||||
_errorHandler(roomName, _localizer.GetString(ChatHubConstants.HopWontKickOp).ToString());
|
_errorHandler(roomName, _localizer.GetString(ChatHubConstants.HopWontKickOp).ToString());
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -319,15 +317,15 @@ namespace Yavsc.Services
|
||||||
}
|
}
|
||||||
|
|
||||||
// all good, time to kick :-)
|
// all good, time to kick :-)
|
||||||
foreach (var ucx in userConnectionIds) {
|
foreach (var ucx in ucxs) {
|
||||||
if (channelInfo.Users.Contains(ucx))
|
if (chanInfo.Users.Contains(ucx))
|
||||||
channelInfo.Users.Remove(ucx);
|
chanInfo.Users.Remove(ucx);
|
||||||
|
|
||||||
else if (channelInfo.Ops.Contains(ucx))
|
else if (chanInfo.Ops.Contains(ucx))
|
||||||
channelInfo.Ops.Remove(ucx);
|
chanInfo.Ops.Remove(ucx);
|
||||||
|
|
||||||
else if (channelInfo.Hops.Contains(ucx))
|
else if (chanInfo.Hops.Contains(ucx))
|
||||||
channelInfo.Hops.Remove(ucx);
|
chanInfo.Hops.Remove(ucx);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -19,7 +19,7 @@ namespace Yavsc.Services
|
||||||
bool Kick(string cxId, string userName, string roomName, string reason);
|
bool Kick(string cxId, string userName, string roomName, string reason);
|
||||||
|
|
||||||
bool Op(string roomName, string userName);
|
bool Op(string roomName, string userName);
|
||||||
bool DeOp(string roomName, string userName);
|
bool Deop(string roomName, string userName);
|
||||||
bool Hop(string roomName, string userName);
|
bool Hop(string roomName, string userName);
|
||||||
bool DeHop(string roomName, string userName);
|
bool DeHop(string roomName, string userName);
|
||||||
bool TryGetChanInfo(string room, out ChatRoomInfo channelInfo);
|
bool TryGetChanInfo(string room, out ChatRoomInfo channelInfo);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue