From 2b97c880b94b3d8abf5d5b1e545fd59f9e6f6b82 Mon Sep 17 00:00:00 2001 From: Blaze34536 <88854346+Blaze34536@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:12:24 -0600 Subject: [PATCH 1/3] chore: updating gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2aa0544..dfda7db 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,5 @@ Thumbs.db .vscode/ .idea/ *.swp -*.swo \ No newline at end of file +*.swo +client_secret.json \ No newline at end of file From 17de049fdb34fdf17b3171c6423d563acabad5a1 Mon Sep 17 00:00:00 2001 From: Blaze34536 <88854346+Blaze34536@users.noreply.github.com> Date: Sun, 16 Nov 2025 19:47:28 -0600 Subject: [PATCH 2/3] feat: purchase_request dto, controller and service --- .../Controllers/PurchaseRequestController.cs | 54 +++++++ .../PurchaseRequestCreateDto.cs | 8 + .../PurchaseRequestResponseDto.cs | 22 +++ .../PurchaseRequestSearchDto.cs | 8 + .../PurchaseRequestUpdateDto.cs | 8 + .../Interfaces/IPurchaseRequestService.cs | 13 ++ .../Models/PurchaseRequestModel.cs | 20 +++ backend/TRFSAE.MemberPortal.API/Program.cs | 38 +++-- .../Services/GoogleSheetsService.cs | 36 +++-- .../Services/PurchaseRequestService.cs | 140 ++++++++++++++++++ 10 files changed, 313 insertions(+), 34 deletions(-) create mode 100644 backend/TRFSAE.MemberPortal.API/Controllers/PurchaseRequestController.cs create mode 100644 backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestCreateDto.cs create mode 100644 backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestResponseDto.cs create mode 100644 backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestSearchDto.cs create mode 100644 backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestUpdateDto.cs create mode 100644 backend/TRFSAE.MemberPortal.API/Interfaces/IPurchaseRequestService.cs create mode 100644 backend/TRFSAE.MemberPortal.API/Models/PurchaseRequestModel.cs create mode 100644 backend/TRFSAE.MemberPortal.API/Services/PurchaseRequestService.cs diff --git a/backend/TRFSAE.MemberPortal.API/Controllers/PurchaseRequestController.cs b/backend/TRFSAE.MemberPortal.API/Controllers/PurchaseRequestController.cs new file mode 100644 index 0000000..8356907 --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/Controllers/PurchaseRequestController.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Mvc; +using TRFSAE.MemberPortal.API.DTOs; +using TRFSAE.MemberPortal.API.Interfaces; +using Supabase; + +namespace TRFSAE.MemberPortal.API.Controllers +{ + [ApiController] + [Route("api/purchase-request")] + public class PurchaseRequestController : ControllerBase + { + private readonly IPurchaseRequestService _purchaseRequestService; + + public PurchaseRequestController(IPurchaseRequestService purchaseRequestService) + { + _purchaseRequestService = purchaseRequestService; + } + + [HttpGet] + public async Task GetAllPurchaseAsync([FromQuery] PurchaseRequestSearchDto? dto) + { + var result = await _purchaseRequestService.GetAllPurchaseRequestsAsync(dto ?? new PurchaseRequestSearchDto()); + return Ok(result); + } + + [HttpGet("{id}")] + public async Task GetPurchaseRequestByIDAsync(Guid id) + { + var item = await _purchaseRequestService.GetPurchaseRequestByIDAsync(id); + return item is null ? NotFound() : Ok(item); + } + + [HttpPost] + public async Task CreatePurchaseRequestAsync(PurchaseRequestCreateDto dto) + { + var created = await _purchaseRequestService.CreatePurchaseRequestAsync(dto); + return CreatedAtAction(nameof(GetPurchaseRequestByIDAsync), new { id = created.Id }, created); + } + + [HttpPut("{id}")] + public async Task UpdatePurchaseRequestByIDAsync(Guid id, PurchaseRequestUpdateDto dto) + { + var updated = await _purchaseRequestService.UpdatePurchaseRequestByIDAsync(id, dto); + return updated is null ? NotFound() : Ok(updated); + } + + [HttpDelete("{id}")] + public async Task DeletePurchaseRequestAsync(Guid id, string confirmationString) + { + var deleted = await _purchaseRequestService.DeletePurchaseRequestAsync(id, confirmationString); + return deleted ? NoContent() : NotFound(); + } + } +} diff --git a/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestCreateDto.cs b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestCreateDto.cs new file mode 100644 index 0000000..7ade908 --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestCreateDto.cs @@ -0,0 +1,8 @@ +namespace TRFSAE.MemberPortal.API.DTOs +{ + public class PurchaseRequestCreateDto + { + public string Status { get; set; } = string.Empty; + public Guid Requester { get; set; } + } +} diff --git a/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestResponseDto.cs b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestResponseDto.cs new file mode 100644 index 0000000..4da3b4f --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestResponseDto.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace TRFSAE.MemberPortal.API.DTOs +{ + public class PurchaseRequestResponseDto + { + [JsonPropertyName("id")] + public Guid Id { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + + [JsonPropertyName("requester")] + public Guid Requester { get; set; } + + [JsonPropertyName("created_at")] + public DateTimeOffset CreatedAt { get; set; } + + [JsonPropertyName("updated_at")] + public DateTime UpdatedAt { get; set; } + } +} diff --git a/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestSearchDto.cs b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestSearchDto.cs new file mode 100644 index 0000000..8554c1b --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestSearchDto.cs @@ -0,0 +1,8 @@ +namespace TRFSAE.MemberPortal.API.DTOs +{ + public class PurchaseRequestSearchDto + { + public string Status { get; set; } = string.Empty; + public Guid? Requester { get; set; } + } +} diff --git a/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestUpdateDto.cs b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestUpdateDto.cs new file mode 100644 index 0000000..58a634e --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestUpdateDto.cs @@ -0,0 +1,8 @@ +namespace TRFSAE.MemberPortal.API.DTOs +{ + public class PurchaseRequestUpdateDto + { + public string Status { get; set; } = string.Empty; + public Guid Requester { get; set; } + } +} diff --git a/backend/TRFSAE.MemberPortal.API/Interfaces/IPurchaseRequestService.cs b/backend/TRFSAE.MemberPortal.API/Interfaces/IPurchaseRequestService.cs new file mode 100644 index 0000000..40b6833 --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/Interfaces/IPurchaseRequestService.cs @@ -0,0 +1,13 @@ +using TRFSAE.MemberPortal.API.DTOs; + +namespace TRFSAE.MemberPortal.API.Interfaces +{ + public interface IPurchaseRequestService + { + Task> GetAllPurchaseRequestsAsync(PurchaseRequestSearchDto dto); + Task GetPurchaseRequestByIDAsync(Guid id); + Task CreatePurchaseRequestAsync(PurchaseRequestCreateDto dto); + Task UpdatePurchaseRequestByIDAsync(Guid id, PurchaseRequestUpdateDto dto); + Task DeletePurchaseRequestAsync(Guid id, string confirmationString); + } +} diff --git a/backend/TRFSAE.MemberPortal.API/Models/PurchaseRequestModel.cs b/backend/TRFSAE.MemberPortal.API/Models/PurchaseRequestModel.cs new file mode 100644 index 0000000..9c40b30 --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/Models/PurchaseRequestModel.cs @@ -0,0 +1,20 @@ +using Supabase.Postgrest.Attributes; +using Supabase.Postgrest.Models; + +namespace TRFSAE.MemberPortal.API.Models +{ + [Table("purchase_request")] + public class PurchaseRequestModel: BaseModel + { + [PrimaryKey("id", false)] + public Guid Id { get; set; } + + public string Status { get; set; } = string.Empty; + + public Guid Requester { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public DateTime UpdatedAt { get; set; } + } +} \ No newline at end of file diff --git a/backend/TRFSAE.MemberPortal.API/Program.cs b/backend/TRFSAE.MemberPortal.API/Program.cs index 3df84b0..04e68aa 100644 --- a/backend/TRFSAE.MemberPortal.API/Program.cs +++ b/backend/TRFSAE.MemberPortal.API/Program.cs @@ -12,6 +12,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); } @@ -35,20 +37,6 @@ return client; }); -// register Supabase client as a singleton for reuse across project -builder.Services.AddScoped(provider => -{ - var options = new SupabaseOptions - { - AutoConnectRealtime = true, - AutoRefreshToken = true, - }; - - var url = builder.Configuration["SupabaseUrl"] ?? throw new InvalidOperationException("Supabase URL is not configured."); - var key = builder.Configuration["SupabaseKey"] ?? throw new InvalidOperationException("Supabase Key is not configured."); - return new Client(url, key, options); -}); - // Add services to the container. // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddEndpointsApiExplorer(); @@ -81,12 +69,22 @@ app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); -using (var scope = app.Services.CreateScope()) -{ - var googleSheetsService = scope.ServiceProvider.GetRequiredService(); - // Initialize Google Sheets API once - await googleSheetsService.ListenToSupabaseChangesAsync(); -} +// Initialize Google Sheets listener after app is built +app.Lifetime.ApplicationStarted.Register(async () => +{ + using (var scope = app.Services.CreateScope()) + { + var googleSheetsService = scope.ServiceProvider.GetRequiredService(); + try + { + await googleSheetsService.ListenToSupabaseChangesAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"Error initializing Google Sheets listener: {ex.Message}"); + } + } +}); app.Run(); diff --git a/backend/TRFSAE.MemberPortal.API/Services/GoogleSheetsService.cs b/backend/TRFSAE.MemberPortal.API/Services/GoogleSheetsService.cs index dabc834..e98055a 100644 --- a/backend/TRFSAE.MemberPortal.API/Services/GoogleSheetsService.cs +++ b/backend/TRFSAE.MemberPortal.API/Services/GoogleSheetsService.cs @@ -84,22 +84,30 @@ public async Task> GetSupabase() public async Task ListenToSupabaseChangesAsync() { - var channel = await _supabaseClient - .From() - .On(Supabase.Realtime.PostgresChanges.PostgresChangesOptions.ListenType.Inserts, async (sender, change) => - { - var items = await GetSupabase(); - if (items != null && items.Count > 0) - { - CreateEntry(items); - } - else + try + { + var channel = await _supabaseClient + .From() + .On(Supabase.Realtime.PostgresChanges.PostgresChangesOptions.ListenType.Inserts, async (sender, change) => { - Console.WriteLine(" No item found to write to Google Sheets"); - } - }); + var items = await GetSupabase(); + if (items != null && items.Count > 0) + { + CreateEntry(items); + } + else + { + Console.WriteLine("No item found to write to Google Sheets"); + } + }); - await channel.Subscribe(); + await channel.Subscribe(); + } + catch (Exception ex) + { + Console.WriteLine($"Error in ListenToSupabaseChangesAsync: {ex.Message}"); + throw; + } } } diff --git a/backend/TRFSAE.MemberPortal.API/Services/PurchaseRequestService.cs b/backend/TRFSAE.MemberPortal.API/Services/PurchaseRequestService.cs new file mode 100644 index 0000000..774db7d --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/Services/PurchaseRequestService.cs @@ -0,0 +1,140 @@ +using TRFSAE.MemberPortal.API.DTOs; +using TRFSAE.MemberPortal.API.Interfaces; +using TRFSAE.MemberPortal.API.Models; +using System.Text.Json; +using Supabase; + +namespace TRFSAE.MemberPortal.API.Services +{ + public class PurchaseRequestService : IPurchaseRequestService + { + private readonly Client _supabaseClient; + + public PurchaseRequestService(Client supabaseClient) + { + _supabaseClient = supabaseClient; + } + + private PurchaseRequestResponseDto MapToDto(PurchaseRequestModel model) + { + return new PurchaseRequestResponseDto + { + Id = model.Id, + Status = model.Status, + Requester = model.Requester, + CreatedAt = model.CreatedAt, + UpdatedAt = model.UpdatedAt + }; + } + + private PurchaseRequestModel MapToModel(PurchaseRequestCreateDto dto) + { + return new PurchaseRequestModel + { + Status = dto.Status, + Requester = dto.Requester + }; + } + + public async Task> GetAllPurchaseRequestsAsync(PurchaseRequestSearchDto dto) + { + var response = await _supabaseClient + .From() + .Get(); + + var results = response.Models.AsEnumerable(); + + if (!string.IsNullOrWhiteSpace(dto.Status)) + { + results = results.Where(x => x.Status == dto.Status); + } + + if (dto.Requester.HasValue && dto.Requester != Guid.Empty) + { + results = results.Where(x => x.Requester == dto.Requester); + } + + return results.Select(MapToDto).ToList(); + } + + public async Task GetPurchaseRequestByIDAsync(Guid id) + { + var response = await _supabaseClient + .From() + .Where(x => x.Id == id) + .Single(); + + if (response == null) + { + throw new Exception("Purchase request not found"); + } + + return MapToDto(response); + } + + public async Task CreatePurchaseRequestAsync(PurchaseRequestCreateDto dto) + { + var newModel = MapToModel(dto); + if (newModel.Id == Guid.Empty) + newModel.Id = Guid.NewGuid(); + if (newModel.CreatedAt == default) + newModel.CreatedAt = DateTimeOffset.UtcNow; + + var response = await _supabaseClient + .From() + .Insert(new List { newModel }); + + if (response.Models is null || response.Models.Count == 0) + throw new Exception("Failed to create purchase request"); + + return MapToDto(response.Models.First()); + } + + public async Task UpdatePurchaseRequestByIDAsync(Guid id, PurchaseRequestUpdateDto dto) + { + var currentRequest = await _supabaseClient + .From() + .Where(x => x.Id == id) + .Single(); + + if (currentRequest == null) + throw new Exception("Purchase request not found"); + + if (!string.IsNullOrWhiteSpace(dto.Status)) + currentRequest.Status = dto.Status; + + if (dto.Requester != Guid.Empty) + currentRequest.Requester = dto.Requester; + + currentRequest.UpdatedAt = DateTime.UtcNow; + + var response = await _supabaseClient + .From() + .Update(currentRequest); + + if (response.Models is null || response.Models.Count == 0) + throw new Exception("Failed to update purchase request"); + + return MapToDto(response.Models.First()); + } + + public async Task DeletePurchaseRequestAsync(Guid id, string confirmationString) + { + if (confirmationString != "confirm") + return false; + + try + { + await _supabaseClient + .From() + .Where(x => x.Id == id) + .Delete(); + return true; + } + catch + { + return false; + } + } + } +} \ No newline at end of file From 58a9f2e95eba4d9dbbd8254a8d979aa63ad5b373 Mon Sep 17 00:00:00 2001 From: Blaze34536 <88854346+Blaze34536@users.noreply.github.com> Date: Sun, 16 Nov 2025 20:49:08 -0600 Subject: [PATCH 3/3] chore: debugging --- .../Controllers/PurchaseRequestController.cs | 8 ++++---- .../PurchaseRequestResponseDto.cs | 2 +- .../Models/PurchaseRequestModel.cs | 16 ++++++++++------ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/backend/TRFSAE.MemberPortal.API/Controllers/PurchaseRequestController.cs b/backend/TRFSAE.MemberPortal.API/Controllers/PurchaseRequestController.cs index 8356907..efc91f0 100644 --- a/backend/TRFSAE.MemberPortal.API/Controllers/PurchaseRequestController.cs +++ b/backend/TRFSAE.MemberPortal.API/Controllers/PurchaseRequestController.cs @@ -23,7 +23,7 @@ public async Task GetAllPurchaseAsync([FromQuery] PurchaseRequest return Ok(result); } - [HttpGet("{id}")] + [HttpGet("{id:guid}", Name = "GetPurchaseRequestById")] public async Task GetPurchaseRequestByIDAsync(Guid id) { var item = await _purchaseRequestService.GetPurchaseRequestByIDAsync(id); @@ -34,17 +34,17 @@ public async Task GetPurchaseRequestByIDAsync(Guid id) public async Task CreatePurchaseRequestAsync(PurchaseRequestCreateDto dto) { var created = await _purchaseRequestService.CreatePurchaseRequestAsync(dto); - return CreatedAtAction(nameof(GetPurchaseRequestByIDAsync), new { id = created.Id }, created); + return CreatedAtRoute("GetPurchaseRequestById", new { id = created.Id }, created); } - [HttpPut("{id}")] + [HttpPut("{id:guid}")] public async Task UpdatePurchaseRequestByIDAsync(Guid id, PurchaseRequestUpdateDto dto) { var updated = await _purchaseRequestService.UpdatePurchaseRequestByIDAsync(id, dto); return updated is null ? NotFound() : Ok(updated); } - [HttpDelete("{id}")] + [HttpDelete("{id:guid}")] public async Task DeletePurchaseRequestAsync(Guid id, string confirmationString) { var deleted = await _purchaseRequestService.DeletePurchaseRequestAsync(id, confirmationString); diff --git a/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestResponseDto.cs b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestResponseDto.cs index 4da3b4f..e2f5d5c 100644 --- a/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestResponseDto.cs +++ b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestResponseDto.cs @@ -11,7 +11,7 @@ public class PurchaseRequestResponseDto public string Status { get; set; } = string.Empty; [JsonPropertyName("requester")] - public Guid Requester { get; set; } + public Guid? Requester { get; set; } [JsonPropertyName("created_at")] public DateTimeOffset CreatedAt { get; set; } diff --git a/backend/TRFSAE.MemberPortal.API/Models/PurchaseRequestModel.cs b/backend/TRFSAE.MemberPortal.API/Models/PurchaseRequestModel.cs index 9c40b30..61d8e19 100644 --- a/backend/TRFSAE.MemberPortal.API/Models/PurchaseRequestModel.cs +++ b/backend/TRFSAE.MemberPortal.API/Models/PurchaseRequestModel.cs @@ -6,15 +6,19 @@ namespace TRFSAE.MemberPortal.API.Models [Table("purchase_request")] public class PurchaseRequestModel: BaseModel { - [PrimaryKey("id", false)] - public Guid Id { get; set; } + [PrimaryKey("id", false)] + public Guid Id { get; set; } - public string Status { get; set; } = string.Empty; + [Column("status")] + public string Status { get; set; } = string.Empty; - public Guid Requester { get; set; } + [Column("requester")] + public Guid Requester { get; set; } - public DateTimeOffset CreatedAt { get; set; } + [Column("created_at")] + public DateTimeOffset CreatedAt { get; set; } - public DateTime UpdatedAt { get; set; } + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } } } \ No newline at end of file